Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

925 lines
24 KiB
Go

package presence
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
"longheng.io/server/internal/platform/db"
)
// ErrNilRedisExecutor 表示 Redis presence manager 没有可用执行器。
var ErrNilRedisExecutor = errors.New("presence redis executor is required")
// redisPresenceTrack 原子维护 session、user 和反向索引。
const redisPresenceTrack = `
local presence_key = KEYS[1]
local session_user_key = KEYS[2]
local sessions_index_key = KEYS[3]
local users_index_key = KEYS[4]
local user_sessions_key = KEYS[5]
local user_sessions_prefix = ARGV[1]
local data = ARGV[2]
local user_id = ARGV[3]
local session_id = ARGV[4]
local ttl = tonumber(ARGV[5]) or 0
local old_user_id = redis.call("GET", session_user_key)
if old_user_id and old_user_id ~= user_id then
local old_user_sessions_key = user_sessions_prefix .. old_user_id
redis.call("SREM", old_user_sessions_key, session_id)
if redis.call("SCARD", old_user_sessions_key) == 0 then
redis.call("SREM", users_index_key, old_user_id)
end
end
if ttl > 0 then
redis.call("SET", presence_key, data, "EX", ttl)
redis.call("SET", session_user_key, user_id, "EX", ttl)
else
redis.call("SET", presence_key, data)
redis.call("SET", session_user_key, user_id)
end
redis.call("SADD", sessions_index_key, session_id)
redis.call("SADD", users_index_key, user_id)
redis.call("SADD", user_sessions_key, session_id)
return "OK"
`
// redisPresenceRemove 会校验 expected_user_id,防止旧 gateway 把新绑定的 session 删除。
const redisPresenceRemove = `
local presence_key = KEYS[1]
local session_user_key = KEYS[2]
local sessions_index_key = KEYS[3]
local users_index_key = KEYS[4]
local user_sessions_prefix = ARGV[1]
local session_id = ARGV[2]
local expected_user_id = ARGV[3]
local user_id = redis.call("GET", session_user_key)
if user_id and user_id ~= "" and expected_user_id ~= "" and user_id ~= expected_user_id then
return "mismatch"
end
if not user_id or user_id == "" then
user_id = expected_user_id
end
redis.call("DEL", presence_key)
redis.call("DEL", session_user_key)
redis.call("SREM", sessions_index_key, session_id)
if user_id and user_id ~= "" then
local user_sessions_key = user_sessions_prefix .. user_id
redis.call("SREM", user_sessions_key, session_id)
if redis.call("SCARD", user_sessions_key) == 0 then
redis.call("SREM", users_index_key, user_id)
end
end
return "removed"
`
// redisPresenceUpdate 在 Redis 内部读取、校验、修改 JSON 并刷新 TTL。
// 这样 Touch/SetStatus 不会发生跨 gateway 读旧值再覆盖新绑定的竞态。
const redisPresenceUpdate = `
local presence_key = KEYS[1]
local session_user_key = KEYS[2]
local expected_user_id = ARGV[1]
local status = ARGV[2]
local metadata_json = ARGV[3]
local metadata_set = ARGV[4]
local updated_at = ARGV[5]
local ttl = tonumber(ARGV[6]) or 0
local user_id = redis.call("GET", session_user_key)
if not user_id or user_id == "" then
return "missing"
end
if expected_user_id ~= "" and user_id ~= expected_user_id then
return "mismatch"
end
local raw = redis.call("GET", presence_key)
if not raw or raw == "" then
return "missing"
end
local ok, item = pcall(cjson.decode, raw)
if not ok then
return "decode_error"
end
if item["user_id"] and item["user_id"] ~= user_id then
return "mismatch"
end
if status ~= "" then
item["status"] = status
end
if metadata_set == "1" then
if metadata_json == "" then
item["metadata"] = nil
else
local meta_ok, metadata = pcall(cjson.decode, metadata_json)
if not meta_ok then
return "decode_error"
end
item["metadata"] = metadata
end
end
item["updated_at"] = updated_at
local data = cjson.encode(item)
if ttl > 0 then
redis.call("SET", presence_key, data, "EX", ttl)
redis.call("SET", session_user_key, user_id, "EX", ttl)
else
redis.call("SET", presence_key, data)
redis.call("SET", session_user_key, user_id)
end
return "updated"
`
// redisSubScript 原子维护订阅和被订阅双索引。
const redisSubScript = `
local subscriptions_index_key = KEYS[1]
local subscribers_index_key = KEYS[2]
local subscriptions_key = KEYS[3]
local subscribers_key = KEYS[4]
local subscriber_id = ARGV[1]
local target_id = ARGV[2]
redis.call("SADD", subscriptions_index_key, subscriber_id)
redis.call("SADD", subscribers_index_key, target_id)
redis.call("SADD", subscriptions_key, target_id)
redis.call("SADD", subscribers_key, subscriber_id)
return "OK"
`
// redisUnsubScript 原子删除订阅和被订阅双索引。
const redisUnsubScript = `
local subscriptions_index_key = KEYS[1]
local subscribers_index_key = KEYS[2]
local subscriptions_key = KEYS[3]
local subscribers_key = KEYS[4]
local subscriber_id = ARGV[1]
local target_id = ARGV[2]
redis.call("SREM", subscriptions_key, target_id)
redis.call("SREM", subscribers_key, subscriber_id)
if redis.call("SCARD", subscriptions_key) == 0 then
redis.call("SREM", subscriptions_index_key, subscriber_id)
end
if redis.call("SCARD", subscribers_key) == 0 then
redis.call("SREM", subscribers_index_key, target_id)
end
return "OK"
`
// RedisOptions 控制 Redis 版 presence manager 的名称、执行器、key 前缀和 TTL。
type RedisOptions struct {
Name string
Executor db.RedisExecutor
KeyPrefix string
Now func() time.Time
TTL time.Duration
}
// RedisManager 使用 Redis 共享在线状态和订阅关系,适合多 gateway 协同。
type RedisManager struct {
initOnce sync.Once
name string
executor db.RedisExecutor
closer interface{ Close() error }
prefix string
now func() time.Time
ttl time.Duration
mu sync.Mutex
}
// NewRedis 创建 Redis 版 presence manager。
func NewRedis(options RedisOptions) *RedisManager {
name := strings.TrimSpace(options.Name)
if name == "" {
name = "redis-presence"
}
prefix := strings.Trim(strings.TrimSpace(options.KeyPrefix), ":")
if prefix == "" {
prefix = "longheng:presence"
}
now := options.Now
if now == nil {
now = time.Now
}
manager := &RedisManager{
name: name,
executor: options.Executor,
closer: redisCloser(options.Executor),
prefix: prefix,
now: now,
ttl: options.TTL,
}
manager.ensureReady()
return manager
}
func redisCloser(executor db.RedisExecutor) interface{ Close() error } {
closer, _ := executor.(interface{ Close() error })
return closer
}
func (m *RedisManager) ensureReady() {
m.initOnce.Do(func() {
m.name = strings.TrimSpace(m.name)
if m.name == "" {
m.name = "redis-presence"
}
m.prefix = strings.Trim(strings.TrimSpace(m.prefix), ":")
if m.prefix == "" {
m.prefix = "longheng:presence"
}
if m.now == nil {
m.now = time.Now
}
if m.closer == nil {
m.closer = redisCloser(m.executor)
}
})
}
func requireRedisManager(m *RedisManager) error {
if m == nil {
return ErrNilRedisExecutor
}
m.ensureReady()
return nil
}
// Close 关闭底层 Redis 连接;没有连接所有权时为空操作。
func (m *RedisManager) Close() error {
if m == nil {
return nil
}
m.ensureReady()
if m.closer == nil {
return nil
}
return m.closer.Close()
}
// Track 注册或替换某个 session 的 Redis 在线状态。
func (m *RedisManager) Track(ctx context.Context, item Presence) (Presence, error) {
if err := ctxErr(ctx); err != nil {
return Presence{}, err
}
if err := requireRedisManager(m); err != nil {
return Presence{}, err
}
item, err := normalizePresence(item, m.now().UTC())
if err != nil {
return Presence{}, err
}
data, err := json.Marshal(item)
if err != nil {
return Presence{}, err
}
// Track 使用 Lua 同时写 session、user 索引和反向索引,避免多 gateway 下索引半更新。
if _, err := m.do(ctx, "EVAL", redisPresenceTrack, 5,
m.presenceKey(item.SessionID),
m.sessionUserKey(item.SessionID),
m.sessionsIndexKey(),
m.usersIndexKey(),
m.userSessionsKey(item.UserID),
m.userSessionsPrefix(),
data,
item.UserID,
item.SessionID,
redisTTLSeconds(m.ttl),
); err != nil {
return Presence{}, err
}
return clonePresence(item), nil
}
// Untrack 移除指定 session 的 Redis 在线状态;userID 非空时必须匹配当前绑定。
func (m *RedisManager) Untrack(ctx context.Context, userID, sessionID string) (Presence, bool, error) {
if err := ctxErr(ctx); err != nil {
return Presence{}, false, err
}
if err := requireRedisManager(m); err != nil {
return Presence{}, false, err
}
userID = strings.TrimSpace(userID)
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return Presence{}, false, ErrInvalidSessionID
}
// 先读当前 session 再按 user 校验删除,避免断线事件误删已重绑的新 session。
m.mu.Lock()
defer m.mu.Unlock()
item, ok, err := m.session(ctx, sessionID)
if err != nil || !ok {
return Presence{}, false, err
}
if userID != "" && item.UserID != userID {
return Presence{}, false, nil
}
removed, err := m.removePresence(ctx, item)
if err != nil {
return Presence{}, false, err
}
if !removed {
return Presence{}, false, nil
}
return clonePresence(item), true, nil
}
// Touch 刷新指定 session 的 Redis 在线更新时间。
func (m *RedisManager) Touch(ctx context.Context, userID, sessionID string, at time.Time) (Presence, bool, error) {
if err := ctxErr(ctx); err != nil {
return Presence{}, false, err
}
if err := requireRedisManager(m); err != nil {
return Presence{}, false, err
}
userID = strings.TrimSpace(userID)
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return Presence{}, false, ErrInvalidSessionID
}
if at.IsZero() {
at = m.now()
}
// Touch 不在客户端侧改 JSON,而是交给 Lua 校验 user 后刷新 updated_at 和 TTL。
updated, err := m.updatePresence(ctx, userID, sessionID, "", nil, false, at.UTC())
if err != nil || !updated {
return Presence{}, false, err
}
item, ok, err := m.session(ctx, sessionID)
if err != nil || !ok {
return Presence{}, false, err
}
return clonePresence(item), true, nil
}
// SetStatus 更新指定 session 的 Redis 在线状态和元数据。
func (m *RedisManager) SetStatus(ctx context.Context, userID, sessionID, status string, metadata map[string]string) (Presence, bool, error) {
if err := ctxErr(ctx); err != nil {
return Presence{}, false, err
}
if err := requireRedisManager(m); err != nil {
return Presence{}, false, err
}
userID = strings.TrimSpace(userID)
sessionID = strings.TrimSpace(sessionID)
status = strings.TrimSpace(status)
if sessionID == "" {
return Presence{}, false, ErrInvalidSessionID
}
if status == "" {
return Presence{}, false, ErrInvalidStatus
}
// metadata 传 nil 表示显式清空;调用方要保留旧 metadata 时不要调用 SetStatus。
updated, err := m.updatePresence(ctx, userID, sessionID, status, normalizeMetadata(metadata), true, m.now().UTC())
if err != nil || !updated {
return Presence{}, false, err
}
item, ok, err := m.session(ctx, sessionID)
if err != nil || !ok {
return Presence{}, false, err
}
return clonePresence(item), true, nil
}
// Presence 查询某个用户在 Redis 中的所有在线 session。
func (m *RedisManager) Presence(ctx context.Context, userID string) ([]Presence, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if err := requireRedisManager(m); err != nil {
return nil, err
}
userID = strings.TrimSpace(userID)
if userID == "" {
return nil, ErrInvalidUserID
}
value, err := m.do(ctx, "SMEMBERS", m.userSessionsKey(userID))
if err != nil {
return nil, err
}
sessionIDs, err := redisStrings(value)
if err != nil {
return nil, err
}
sort.Strings(sessionIDs)
out := make([]Presence, 0, len(sessionIDs))
for _, sessionID := range sessionIDs {
item, ok, err := m.session(ctx, sessionID)
if err != nil {
return nil, err
}
if !ok {
// 读到过期 session 时顺手清理用户反向索引,降低长期高 churn 下的脏索引。
_, _ = m.do(ctx, "SREM", m.userSessionsKey(userID), sessionID)
continue
}
out = append(out, clonePresence(item))
}
_ = m.cleanupUserIndex(ctx, userID)
sortPresences(out)
return out, nil
}
// Session 查询 Redis 中指定 session 的在线状态。
func (m *RedisManager) Session(ctx context.Context, sessionID string) (Presence, bool, error) {
if err := ctxErr(ctx); err != nil {
return Presence{}, false, err
}
if err := requireRedisManager(m); err != nil {
return Presence{}, false, err
}
return m.session(ctx, sessionID)
}
// Subscribe 在 Redis 中订阅目标用户在线状态变化。
func (m *RedisManager) Subscribe(ctx context.Context, subscriberID, targetID string) error {
if err := ctxErr(ctx); err != nil {
return err
}
if err := requireRedisManager(m); err != nil {
return err
}
// Subscribe 通过 Lua 同时维护 subscriber->targets 和 target->subscribers 两个索引,避免并发订阅留下半边索引。
subscriberID, targetID, err := normSubIDs(subscriberID, targetID)
if err != nil {
return err
}
_, err = m.do(ctx, "EVAL", redisSubScript, 4,
m.subIndexKey(),
m.subscribersIndexKey(),
m.subscriptionsKey(subscriberID),
m.subscribersKey(targetID),
subscriberID,
targetID,
)
return err
}
// Unsubscribe 在 Redis 中取消订阅目标用户在线状态变化。
func (m *RedisManager) Unsubscribe(ctx context.Context, subscriberID, targetID string) error {
if err := ctxErr(ctx); err != nil {
return err
}
if err := requireRedisManager(m); err != nil {
return err
}
// Unsubscribe 也用 Lua 做双索引删除;当集合为空时同步清 index,降低 stale cleanup 压力。
subscriberID, targetID, err := normSubIDs(subscriberID, targetID)
if err != nil {
return err
}
_, err = m.do(ctx, "EVAL", redisUnsubScript, 4,
m.subIndexKey(),
m.subscribersIndexKey(),
m.subscriptionsKey(subscriberID),
m.subscribersKey(targetID),
subscriberID,
targetID,
)
return err
}
// Subscribers 返回 Redis 中订阅目标用户的所有订阅者。
func (m *RedisManager) Subscribers(ctx context.Context, targetID string) ([]string, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if err := requireRedisManager(m); err != nil {
return nil, err
}
targetID = strings.TrimSpace(targetID)
if targetID == "" {
return nil, ErrInvalidUserID
}
value, err := m.do(ctx, "SMEMBERS", m.subscribersKey(targetID))
if err != nil {
return nil, err
}
out, err := redisStrings(value)
if err != nil {
return nil, err
}
sort.Strings(out)
return out, nil
}
// Subscriptions 返回 Redis 中订阅者关注的所有目标用户。
func (m *RedisManager) Subscriptions(ctx context.Context, subscriberID string) ([]string, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if err := requireRedisManager(m); err != nil {
return nil, err
}
subscriberID = strings.TrimSpace(subscriberID)
if subscriberID == "" {
return nil, ErrInvalidUserID
}
value, err := m.do(ctx, "SMEMBERS", m.subscriptionsKey(subscriberID))
if err != nil {
return nil, err
}
out, err := redisStrings(value)
if err != nil {
return nil, err
}
sort.Strings(out)
return out, nil
}
// Broadcast 将 Redis 中目标用户当前在线状态广播给所有订阅者。
func (m *RedisManager) Broadcast(ctx context.Context, targetID string, send SendFunc) error {
if send == nil {
return ErrNilSender
}
if err := ctxErr(ctx); err != nil {
return err
}
if err := requireRedisManager(m); err != nil {
return err
}
subscribers, err := m.Subscribers(ctx, targetID)
if err != nil {
return err
}
presences, err := m.Presence(ctx, targetID)
if err != nil {
return err
}
// Broadcast 只负责 fanout 当前在线状态,不提供 ack 或离线补偿。
// 需要可靠通知的业务应把事件先落 EventLog/Outbox。
for _, subscriberID := range subscribers {
if err := ctxErr(ctx); err != nil {
return err
}
if err := send(ctx, subscriberID, clonePresences(presences)); err != nil {
return err
}
}
return nil
}
// Snapshot 返回 Redis presence manager 当前快照;Redis 不可用时返回带名称的空快照。
func (m *RedisManager) Snapshot() Snapshot {
if m == nil {
return Snapshot{}
}
m.ensureReady()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
snapshot, err := m.snapshot(ctx)
if err != nil {
return Snapshot{Name: m.name}
}
return snapshot
}
func (m *RedisManager) session(ctx context.Context, sessionID string) (Presence, bool, error) {
if err := requireRedisManager(m); err != nil {
return Presence{}, false, err
}
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return Presence{}, false, ErrInvalidSessionID
}
value, err := m.do(ctx, "GET", m.presenceKey(sessionID))
if err != nil {
return Presence{}, false, err
}
data, ok, err := redisBytes(value)
if err != nil || !ok {
return Presence{}, ok, err
}
var item Presence
if err := json.Unmarshal(data, &item); err != nil {
return Presence{}, false, fmt.Errorf("decode presence: %w", err)
}
item.Metadata = normalizeMetadata(item.Metadata)
return item, true, nil
}
func (m *RedisManager) updatePresence(ctx context.Context, userID, sessionID, status string, metadata map[string]string, updateMetadata bool, updatedAt time.Time) (bool, error) {
if err := requireRedisManager(m); err != nil {
return false, err
}
metadataJSON := ""
if metadata != nil {
data, err := json.Marshal(metadata)
if err != nil {
return false, err
}
metadataJSON = string(data)
}
statusText, err := redisString(m.do(ctx, "EVAL", redisPresenceUpdate, 2,
m.presenceKey(sessionID),
m.sessionUserKey(sessionID),
userID,
status,
metadataJSON,
boolRedisArg(updateMetadata),
updatedAt.Format(time.RFC3339Nano),
redisTTLSeconds(m.ttl),
))
if err != nil {
return false, err
}
switch statusText {
case "updated":
return true, nil
case "missing", "mismatch":
return false, nil
case "decode_error":
return false, fmt.Errorf("decode presence")
default:
return false, fmt.Errorf("unexpected redis presence update status %q", statusText)
}
}
func boolRedisArg(value bool) string {
if value {
return "1"
}
return "0"
}
func (m *RedisManager) removePresence(ctx context.Context, item Presence) (bool, error) {
if err := requireRedisManager(m); err != nil {
return false, err
}
status, err := redisString(m.do(ctx, "EVAL", redisPresenceRemove, 4,
m.presenceKey(item.SessionID),
m.sessionUserKey(item.SessionID),
m.sessionsIndexKey(),
m.usersIndexKey(),
m.userSessionsPrefix(),
item.SessionID,
item.UserID,
))
if err != nil {
return false, err
}
return status == "removed", nil
}
func (m *RedisManager) snapshot(ctx context.Context) (Snapshot, error) {
if err := requireRedisManager(m); err != nil {
return Snapshot{}, err
}
sessionIDs, err := m.redisSet(ctx, m.sessionsIndexKey())
if err != nil {
return Snapshot{}, err
}
presences := make([]Presence, 0, len(sessionIDs))
for _, sessionID := range sessionIDs {
item, ok, err := m.session(ctx, sessionID)
if err != nil {
return Snapshot{}, err
}
if !ok {
// Snapshot 是管理面读路径,可以顺带清理过期 session 索引。
_, _ = m.do(ctx, "SREM", m.sessionsIndexKey(), sessionID)
continue
}
presences = append(presences, item)
}
sortPresences(presences)
userIDs, err := m.redisSet(ctx, m.usersIndexKey())
if err != nil {
return Snapshot{}, err
}
userCount := 0
for _, userID := range userIDs {
presences, err := m.Presence(ctx, userID)
if err != nil {
return Snapshot{}, err
}
if len(presences) > 0 {
userCount++
}
}
subscriberIDs, err := m.redisSet(ctx, m.subIndexKey())
if err != nil {
return Snapshot{}, err
}
subscriptions := make([]Subscription, 0)
for _, subscriberID := range subscriberIDs {
targets, err := m.Subscriptions(ctx, subscriberID)
if err != nil {
return Snapshot{}, err
}
if len(targets) == 0 {
// 订阅者没有任何目标时清掉全局索引,避免长期压测后列表膨胀。
_, _ = m.do(ctx, "SREM", m.subIndexKey(), subscriberID)
continue
}
for _, targetID := range targets {
subscriptions = append(subscriptions, Subscription{SubscriberID: subscriberID, TargetID: targetID})
}
}
sortSubscriptions(subscriptions)
return Snapshot{
Name: m.name,
PresenceCount: len(presences),
UserCount: userCount,
SubscriptionCount: len(subscriptions),
Presences: presences,
Subscriptions: subscriptions,
}, nil
}
func (m *RedisManager) cleanupUserIndex(ctx context.Context, userID string) error {
if err := requireRedisManager(m); err != nil {
return err
}
count, err := m.redisInt(ctx, "SCARD", m.userSessionsKey(userID))
if err != nil {
return err
}
if count == 0 {
_, err = m.do(ctx, "SREM", m.usersIndexKey(), userID)
}
return err
}
func (m *RedisManager) redisSet(ctx context.Context, key string) ([]string, error) {
if err := requireRedisManager(m); err != nil {
return nil, err
}
value, err := m.do(ctx, "SMEMBERS", key)
if err != nil {
return nil, err
}
out, err := redisStrings(value)
if err != nil {
return nil, err
}
sort.Strings(out)
return out, nil
}
func (m *RedisManager) redisInt(ctx context.Context, command string, args ...any) (int64, error) {
if err := requireRedisManager(m); err != nil {
return 0, err
}
value, err := m.do(ctx, command, args...)
if err != nil {
return 0, err
}
return redisInt64(value)
}
func (m *RedisManager) do(ctx context.Context, command string, args ...any) (any, error) {
if err := ctxErr(ctx); err != nil {
return nil, err
}
if err := requireRedisManager(m); err != nil {
return nil, err
}
if m.executor == nil {
return nil, ErrNilRedisExecutor
}
return m.executor.Do(ctx, command, args...)
}
func (m *RedisManager) presenceKey(sessionID string) string {
return m.prefix + ":session:" + sessionID
}
func (m *RedisManager) sessionUserKey(sessionID string) string {
return m.prefix + ":session_user:" + sessionID
}
func (m *RedisManager) userSessionsKey(userID string) string {
return m.prefix + ":user_sessions:" + userID
}
func (m *RedisManager) userSessionsPrefix() string {
return m.prefix + ":user_sessions:"
}
func (m *RedisManager) sessionsIndexKey() string {
return m.prefix + ":sessions"
}
func (m *RedisManager) usersIndexKey() string {
return m.prefix + ":users"
}
func (m *RedisManager) subscriptionsKey(subscriberID string) string {
return m.prefix + ":subscriptions:" + subscriberID
}
func (m *RedisManager) subscribersKey(targetID string) string {
return m.prefix + ":subscribers:" + targetID
}
func (m *RedisManager) subIndexKey() string {
return m.prefix + ":subscriptions"
}
func (m *RedisManager) subscribersIndexKey() string {
return m.prefix + ":subscribers"
}
func redisTTLSeconds(ttl time.Duration) int64 {
if ttl <= 0 {
return 0
}
seconds := int64(ttl / time.Second)
if seconds <= 0 {
return 1
}
return seconds
}
func redisBytes(value any) ([]byte, bool, error) {
switch v := value.(type) {
case nil:
return nil, false, nil
case []byte:
return append([]byte(nil), v...), true, nil
case string:
return []byte(v), true, nil
default:
return nil, false, fmt.Errorf("unexpected redis bulk type %T", value)
}
}
func redisString(value any, err error) (string, error) {
if err != nil {
return "", err
}
switch v := value.(type) {
case nil:
return "", nil
case []byte:
return string(v), nil
case string:
return v, nil
default:
return "", fmt.Errorf("unexpected redis string type %T", value)
}
}
func redisStrings(value any) ([]string, error) {
switch v := value.(type) {
case nil:
return nil, nil
case []string:
return append([]string(nil), v...), nil
case [][]byte:
out := make([]string, len(v))
for i := range v {
out[i] = string(v[i])
}
return out, nil
case []any:
out := make([]string, 0, len(v))
for _, item := range v {
switch x := item.(type) {
case []byte:
out = append(out, string(x))
case string:
out = append(out, x)
default:
return nil, fmt.Errorf("unexpected redis string item %T", item)
}
}
return out, nil
default:
return nil, fmt.Errorf("unexpected redis string slice type %T", value)
}
}
func redisInt64(value any) (int64, error) {
switch v := value.(type) {
case int:
return int64(v), nil
case int64:
return v, nil
case uint64:
const maxInt64 = uint64(1<<63 - 1)
if v > maxInt64 {
return 0, fmt.Errorf("redis integer overflows int64: %d", v)
}
return int64(v), nil
case []byte:
return strconv.ParseInt(string(v), 10, 64)
case string:
return strconv.ParseInt(v, 10, 64)
default:
return 0, fmt.Errorf("unexpected redis integer type %T", value)
}
}