Files
truewhile b0fe40142a Rebrand MMTL to MeBox (name, logo, Docker image) (#17)
* Rebrand MMTL to MeBox across codebase and assets

Rename the project display name, Go module path, environment variable
prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl
to MeBox/mebox. Replace logo assets with the new MeBox icon and keep
legacy SQLite migration support for existing mmtl.db deployments.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix logo icons: use cube-only crop without truncated text

Previous icon generation cropped too much of the source image, including
partial MeBox wordmark text that was cut off in square icon containers.
Regenerate logo-64/192/512, favicon, and SVG from cube-only region.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 16:26:28 +08:00

194 lines
4.2 KiB
Go

package service
import (
"context"
"encoding/json"
"strings"
"sync"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"github.com/truewhile/MeBox/internal/config"
)
type RuntimeCacheService struct {
log *zap.Logger
client *redis.Client
prefix string
mu sync.RWMutex
memory map[string]runtimeCacheItem
limit int
}
type runtimeCacheItem struct {
raw []byte
expiresAt time.Time
}
func NewRuntimeCacheService(cfg *config.Config, log *zap.Logger) *RuntimeCacheService {
c := &RuntimeCacheService{log: log, memory: map[string]runtimeCacheItem{}, limit: 2048}
if cfg == nil {
return c
}
c.prefix = strings.Trim(strings.TrimSpace(cfg.Cache.RedisPrefix), ":")
if c.prefix == "" {
c.prefix = "mebox"
}
rawURL := strings.TrimSpace(cfg.Cache.RedisURL)
if rawURL == "" {
return c
}
opts, err := redis.ParseURL(rawURL)
if err != nil {
if log != nil {
log.Warn("redis cache disabled: invalid redis url", zap.Error(err))
}
return c
}
client := redis.NewClient(opts)
pingCtx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond)
defer cancel()
if err := client.Ping(pingCtx).Err(); err != nil {
if log != nil {
log.Warn("redis cache unavailable; using in-process cache", zap.Error(err))
}
_ = client.Close()
return c
}
c.client = client
if log != nil {
log.Info("redis runtime cache enabled with in-process L1", zap.String("addr", opts.Addr), zap.String("prefix", c.prefix))
}
return c
}
func (c *RuntimeCacheService) Enabled() bool {
return c != nil
}
func (c *RuntimeCacheService) Close() error {
if c == nil || c.client == nil {
return nil
}
return c.client.Close()
}
func (c *RuntimeCacheService) GetJSON(ctx context.Context, key string, out any) bool {
if !c.Enabled() || strings.TrimSpace(key) == "" || out == nil {
return false
}
fullKey := c.key(key)
if raw, ok := c.getMemory(fullKey); ok {
return json.Unmarshal(raw, out) == nil
}
if c.client != nil {
raw, err := c.client.Get(ctx, fullKey).Bytes()
if err == nil {
if json.Unmarshal(raw, out) != nil {
return false
}
c.setMemory(fullKey, raw, 2*time.Second)
return true
}
}
return false
}
func (c *RuntimeCacheService) SetJSON(ctx context.Context, key string, value any, ttl time.Duration) {
if !c.Enabled() || strings.TrimSpace(key) == "" || value == nil || ttl <= 0 {
return
}
raw, err := json.Marshal(value)
if err != nil {
return
}
fullKey := c.key(key)
c.setMemory(fullKey, raw, ttl)
if c.client != nil {
_ = c.client.Set(ctx, fullKey, raw, ttl).Err()
}
}
func (c *RuntimeCacheService) DeletePrefix(ctx context.Context, prefix string) {
if !c.Enabled() || strings.TrimSpace(prefix) == "" {
return
}
fullPrefix := c.key(prefix)
c.deleteMemoryPrefix(fullPrefix)
if c.client != nil {
pattern := fullPrefix + "*"
var cursor uint64
for {
keys, next, err := c.client.Scan(ctx, cursor, pattern, 200).Result()
if err != nil {
return
}
if len(keys) > 0 {
_ = c.client.Del(ctx, keys...).Err()
}
cursor = next
if cursor == 0 {
return
}
}
}
}
func (c *RuntimeCacheService) key(key string) string {
key = strings.TrimLeft(strings.TrimSpace(key), ":")
if c.prefix == "" {
return key
}
return c.prefix + ":" + key
}
func (c *RuntimeCacheService) getMemory(key string) ([]byte, bool) {
now := time.Now()
c.mu.RLock()
item, ok := c.memory[key]
c.mu.RUnlock()
if !ok {
return nil, false
}
if now.After(item.expiresAt) {
c.mu.Lock()
delete(c.memory, key)
c.mu.Unlock()
return nil, false
}
return item.raw, true
}
func (c *RuntimeCacheService) setMemory(key string, raw []byte, ttl time.Duration) {
if ttl <= 0 || len(raw) == 0 {
return
}
c.mu.Lock()
defer c.mu.Unlock()
if len(c.memory) >= c.limit {
now := time.Now()
for k, item := range c.memory {
if now.After(item.expiresAt) || len(c.memory) >= c.limit {
delete(c.memory, k)
}
if len(c.memory) < c.limit {
break
}
}
}
c.memory[key] = runtimeCacheItem{raw: append([]byte(nil), raw...), expiresAt: time.Now().Add(ttl)}
}
func (c *RuntimeCacheService) deleteMemoryPrefix(prefix string) {
c.mu.Lock()
defer c.mu.Unlock()
for key := range c.memory {
if strings.HasPrefix(key, prefix) {
delete(c.memory, key)
}
}
}