Compare commits

..

No commits in common. "50f4cb91f6a32c0a38195679ef1d09a0b46ac8c4" and "c952242a9cad8c481478ca05f02e09f5b5017785" have entirely different histories.

4 changed files with 109 additions and 430 deletions

View File

@ -1,11 +1,11 @@
package sessions package sessions
import ( import (
"crypto/rand"
"encoding/base64"
"sync" "sync"
"time" "time"
"github.com/VictoriaMetrics/fastcache"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@ -14,12 +14,11 @@ const (
DefaultCookieName = "MoonsharkSID" DefaultCookieName = "MoonsharkSID"
DefaultCookiePath = "/" DefaultCookiePath = "/"
DefaultMaxAge = 86400 // 1 day in seconds DefaultMaxAge = 86400 // 1 day in seconds
CleanupInterval = 5 * time.Minute
) )
// SessionManager handles multiple sessions // SessionManager handles multiple sessions
type SessionManager struct { type SessionManager struct {
cache *fastcache.Cache sessions map[string]*Session
maxSessions int maxSessions int
cookieName string cookieName string
cookiePath string cookiePath string
@ -27,19 +26,7 @@ type SessionManager struct {
cookieSecure bool cookieSecure bool
cookieHTTPOnly bool cookieHTTPOnly bool
cookieMaxAge int cookieMaxAge int
cookieMu sync.RWMutex // Only cookie options need a mutex mu sync.RWMutex
cleanupTicker *time.Ticker
cleanupDone chan struct{}
}
// InitializeSessionPool pre-allocates session objects
func InitializeSessionPool(size int) {
for range size {
session := &Session{
Data: make(map[string]any, 8),
}
ReturnToPool(session)
}
} }
// NewSessionManager creates a new session manager // NewSessionManager creates a new session manager
@ -48,67 +35,44 @@ func NewSessionManager(maxSessions int) *SessionManager {
maxSessions = DefaultMaxSessions maxSessions = DefaultMaxSessions
} }
// Estimate max memory: ~4KB per session × maxSessions return &SessionManager{
maxBytes := maxSessions * 4096 sessions: make(map[string]*Session, maxSessions),
sm := &SessionManager{
cache: fastcache.New(maxBytes),
maxSessions: maxSessions, maxSessions: maxSessions,
cookieName: DefaultCookieName, cookieName: DefaultCookieName,
cookiePath: DefaultCookiePath, cookiePath: DefaultCookiePath,
cookieHTTPOnly: true, cookieHTTPOnly: true,
cookieMaxAge: DefaultMaxAge, cookieMaxAge: DefaultMaxAge,
cleanupDone: make(chan struct{}),
}
// Pre-allocate session objects for common pool size
InitializeSessionPool(100) // Adjust based on expected concurrent requests
// Start periodic cleanup
sm.cleanupTicker = time.NewTicker(CleanupInterval)
go sm.cleanupRoutine()
return sm
}
// Stop shuts down the session manager's cleanup routine
func (sm *SessionManager) Stop() {
close(sm.cleanupDone)
}
// cleanupRoutine periodically removes expired sessions
func (sm *SessionManager) cleanupRoutine() {
for {
select {
case <-sm.cleanupTicker.C:
sm.CleanupExpired()
case <-sm.cleanupDone:
sm.cleanupTicker.Stop()
return
}
} }
} }
// generateSessionID creates a random session ID // generateSessionID creates a random session ID
func generateSessionID() string { func generateSessionID() string {
id, _ := gonanoid.New() b := make([]byte, 32)
return id if _, err := rand.Read(b); err != nil {
return time.Now().String() // Fallback
}
return base64.URLEncoding.EncodeToString(b)
} }
// GetSession retrieves a session by ID, or creates a new one if it doesn't exist // GetSession retrieves a session by ID, or creates a new one if it doesn't exist
func (sm *SessionManager) GetSession(id string) *Session { func (sm *SessionManager) GetSession(id string) *Session {
// Try to get an existing session // Try to get an existing session
if id != "" { if id != "" {
data := sm.cache.Get(nil, []byte(id)) sm.mu.RLock()
if len(data) > 0 { session, exists := sm.sessions[id]
session, err := Unmarshal(data) sm.mu.RUnlock()
if err == nil && !session.IsExpired() {
if exists {
// Check if session is expired
if session.IsExpired() {
sm.mu.Lock()
delete(sm.sessions, id)
sm.mu.Unlock()
} else {
// Update last used time
session.UpdateLastUsed() session.UpdateLastUsed()
session.ResetDirty() // Start clean
return session return session
} }
// Session expired or corrupt, remove it
sm.cache.Del([]byte(id))
} }
} }
@ -119,55 +83,72 @@ func (sm *SessionManager) GetSession(id string) *Session {
// CreateSession generates a new session with a unique ID // CreateSession generates a new session with a unique ID
func (sm *SessionManager) CreateSession() *Session { func (sm *SessionManager) CreateSession() *Session {
id := generateSessionID() id := generateSessionID()
// Ensure ID uniqueness
attempts := 0
for attempts < 3 {
if sm.cache.Has([]byte(id)) {
id = generateSessionID()
attempts++
} else {
break
}
}
session := NewSession(id, sm.cookieMaxAge) session := NewSession(id, sm.cookieMaxAge)
// Serialize and store the session sm.mu.Lock()
if data, err := session.Marshal(); err == nil { // Enforce session limit - evict LRU if needed
sm.cache.Set([]byte(id), data) if len(sm.sessions) >= sm.maxSessions {
sm.evictLRU()
} }
session.ResetDirty() // Start clean sm.sessions[id] = session
sm.mu.Unlock()
return session return session
} }
// evictLRU removes the least recently used session
func (sm *SessionManager) evictLRU() {
// Called with mutex already held
if len(sm.sessions) == 0 {
return
}
var oldestID string
var oldestTime time.Time
// Find oldest session
for id, session := range sm.sessions {
if oldestID == "" || session.LastUsed.Before(oldestTime) {
oldestID = id
oldestTime = session.LastUsed
}
}
if oldestID != "" {
delete(sm.sessions, oldestID)
}
}
// DestroySession removes a session // DestroySession removes a session
func (sm *SessionManager) DestroySession(id string) { func (sm *SessionManager) DestroySession(id string) {
// Get and clean session from cache before deleting sm.mu.Lock()
data := sm.cache.Get(nil, []byte(id)) defer sm.mu.Unlock()
if len(data) > 0 { delete(sm.sessions, id)
if session, err := Unmarshal(data); err == nil {
ReturnToPool(session)
}
}
sm.cache.Del([]byte(id))
} }
// CleanupExpired removes all expired sessions // CleanupExpired removes all expired sessions
// Note: fastcache doesn't provide iteration, so we can't clean all expired sessions
// This is a limitation of this implementation
func (sm *SessionManager) CleanupExpired() int { func (sm *SessionManager) CleanupExpired() int {
// No way to iterate through all keys in fastcache removed := 0
// We'd need to track expiring sessions separately now := time.Now()
return 0
sm.mu.Lock()
defer sm.mu.Unlock()
for id, session := range sm.sessions {
if now.After(session.Expiry) {
delete(sm.sessions, id)
removed++
}
}
return removed
} }
// SetCookieOptions configures cookie parameters // SetCookieOptions configures cookie parameters
func (sm *SessionManager) SetCookieOptions(name, path, domain string, secure, httpOnly bool, maxAge int) { func (sm *SessionManager) SetCookieOptions(name, path, domain string, secure, httpOnly bool, maxAge int) {
sm.cookieMu.Lock() sm.mu.Lock()
defer sm.cookieMu.Unlock() defer sm.mu.Unlock()
sm.cookieName = name sm.cookieName = name
sm.cookiePath = path sm.cookiePath = path
@ -179,11 +160,7 @@ func (sm *SessionManager) SetCookieOptions(name, path, domain string, secure, ht
// GetSessionFromRequest extracts the session from a request // GetSessionFromRequest extracts the session from a request
func (sm *SessionManager) GetSessionFromRequest(ctx *fasthttp.RequestCtx) *Session { func (sm *SessionManager) GetSessionFromRequest(ctx *fasthttp.RequestCtx) *Session {
sm.cookieMu.RLock() cookie := ctx.Request.Header.Cookie(sm.cookieName)
cookieName := sm.cookieName
sm.cookieMu.RUnlock()
cookie := ctx.Request.Header.Cookie(cookieName)
if len(cookie) == 0 { if len(cookie) == 0 {
return sm.CreateSession() return sm.CreateSession()
} }
@ -196,43 +173,25 @@ func (sm *SessionManager) ApplySessionCookie(ctx *fasthttp.RequestCtx, session *
cookie := fasthttp.AcquireCookie() cookie := fasthttp.AcquireCookie()
defer fasthttp.ReleaseCookie(cookie) defer fasthttp.ReleaseCookie(cookie)
// Get cookie options with minimal lock time cookie.SetKey(sm.cookieName)
sm.cookieMu.RLock()
cookieName := sm.cookieName
cookiePath := sm.cookiePath
cookieDomain := sm.cookieDomain
cookieSecure := sm.cookieSecure
cookieHTTPOnly := sm.cookieHTTPOnly
cookieMaxAge := sm.cookieMaxAge
sm.cookieMu.RUnlock()
// Store updated session only if it has changes
if session.IsDirty() {
if data, err := session.Marshal(); err == nil {
sm.cache.Set([]byte(session.ID), data)
}
session.ResetDirty()
}
cookie.SetKey(cookieName)
cookie.SetValue(session.ID) cookie.SetValue(session.ID)
cookie.SetPath(cookiePath) cookie.SetPath(sm.cookiePath)
cookie.SetHTTPOnly(cookieHTTPOnly) cookie.SetHTTPOnly(sm.cookieHTTPOnly)
cookie.SetMaxAge(cookieMaxAge) cookie.SetMaxAge(sm.cookieMaxAge)
if cookieDomain != "" { if sm.cookieDomain != "" {
cookie.SetDomain(cookieDomain) cookie.SetDomain(sm.cookieDomain)
} }
cookie.SetSecure(cookieSecure) cookie.SetSecure(sm.cookieSecure)
ctx.Response.Header.SetCookie(cookie) ctx.Response.Header.SetCookie(cookie)
} }
// CookieOptions returns the cookie options for this session manager // CookieOptions returns the cookie options for this session manager
func (sm *SessionManager) CookieOptions() map[string]any { func (sm *SessionManager) CookieOptions() map[string]any {
sm.cookieMu.RLock() sm.mu.RLock()
defer sm.cookieMu.RUnlock() defer sm.mu.RUnlock()
return map[string]any{ return map[string]any{
"name": sm.cookieName, "name": sm.cookieName,

View File

@ -1,11 +1,9 @@
package sessions package sessions
import ( import (
"maps"
"sync" "sync"
"time" "time"
"github.com/deneonet/benc"
bstd "github.com/deneonet/benc/std"
) )
// Session stores data for a single user session // Session stores data for a single user session
@ -16,96 +14,61 @@ type Session struct {
UpdatedAt time.Time UpdatedAt time.Time
LastUsed time.Time LastUsed time.Time
Expiry time.Time Expiry time.Time
dirty bool // Tracks if session has changes, not serialized mu sync.RWMutex
}
// Session pool to reduce allocations
var sessionPool = sync.Pool{
New: func() any {
return &Session{
Data: make(map[string]any, 8),
}
},
}
// BufPool for reusing serialization buffers
var bufPool = benc.NewBufPool(benc.WithBufferSize(4096))
// GetFromPool retrieves a session from the pool
func GetFromPool() *Session {
return sessionPool.Get().(*Session)
}
// ReturnToPool returns a session to the pool after cleaning it
func ReturnToPool(s *Session) {
if s == nil {
return
}
// Clean the session for reuse
s.ID = ""
for k := range s.Data {
delete(s.Data, k)
}
s.CreatedAt = time.Time{}
s.UpdatedAt = time.Time{}
s.LastUsed = time.Time{}
s.Expiry = time.Time{}
s.dirty = false
sessionPool.Put(s)
} }
// NewSession creates a new session with the given ID // NewSession creates a new session with the given ID
func NewSession(id string, maxAge int) *Session { func NewSession(id string, maxAge int) *Session {
now := time.Now() now := time.Now()
return &Session{
// Get from pool or create new ID: id,
session := GetFromPool() Data: make(map[string]any),
CreatedAt: now,
// Initialize UpdatedAt: now,
session.ID = id LastUsed: now,
session.CreatedAt = now Expiry: now.Add(time.Duration(maxAge) * time.Second),
session.UpdatedAt = now }
session.LastUsed = now
session.Expiry = now.Add(time.Duration(maxAge) * time.Second)
session.dirty = false
return session
} }
// Get retrieves a value from the session // Get retrieves a value from the session
func (s *Session) Get(key string) any { func (s *Session) Get(key string) any {
s.mu.RLock()
defer s.mu.RUnlock()
return s.Data[key] return s.Data[key]
} }
// Set stores a value in the session // Set stores a value in the session
func (s *Session) Set(key string, value any) { func (s *Session) Set(key string, value any) {
s.mu.Lock()
defer s.mu.Unlock()
s.Data[key] = value s.Data[key] = value
s.UpdatedAt = time.Now() s.UpdatedAt = time.Now()
s.dirty = true
} }
// Delete removes a value from the session // Delete removes a value from the session
func (s *Session) Delete(key string) { func (s *Session) Delete(key string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.Data, key) delete(s.Data, key)
s.UpdatedAt = time.Now() s.UpdatedAt = time.Now()
s.dirty = true
} }
// Clear removes all data from the session // Clear removes all data from the session
func (s *Session) Clear() { func (s *Session) Clear() {
s.Data = make(map[string]any, 8) s.mu.Lock()
defer s.mu.Unlock()
s.Data = make(map[string]any)
s.UpdatedAt = time.Now() s.UpdatedAt = time.Now()
s.dirty = true
} }
// GetAll returns a copy of all session data // GetAll returns a copy of all session data
func (s *Session) GetAll() map[string]any { func (s *Session) GetAll() map[string]any {
s.mu.RLock()
defer s.mu.RUnlock()
copy := make(map[string]any, len(s.Data)) copy := make(map[string]any, len(s.Data))
for k, v := range s.Data { maps.Copy(copy, s.Data)
copy[k] = v
}
return copy return copy
} }
@ -115,236 +78,8 @@ func (s *Session) IsExpired() bool {
} }
// UpdateLastUsed updates the last used time // UpdateLastUsed updates the last used time
// Only updates if at least 5 seconds have passed since last update
func (s *Session) UpdateLastUsed() { func (s *Session) UpdateLastUsed() {
now := time.Now() s.mu.Lock()
if now.Sub(s.LastUsed) > 5*time.Second { s.LastUsed = time.Now()
s.LastUsed = now s.mu.Unlock()
// Not marking dirty for LastUsed updates to reduce writes
}
}
// IsDirty returns if the session has unsaved changes
func (s *Session) IsDirty() bool {
return s.dirty
}
// ResetDirty marks the session as clean after saving
func (s *Session) ResetDirty() {
s.dirty = false
}
// SizePlain calculates the size needed to marshal the session
func (s *Session) SizePlain() (size int) {
// ID
size += bstd.SizeString(s.ID)
// Data map
size += bstd.SizeMap(s.Data, bstd.SizeString, func(v any) int {
return sizeAny(v)
})
// Time fields stored as int64 Unix timestamps
size += bstd.SizeInt64() * 4
return size
}
// MarshalPlain serializes the session to binary
func (s *Session) MarshalPlain(n int, b []byte) int {
// ID
n = bstd.MarshalString(n, b, s.ID)
// Data map
n = bstd.MarshalMap(n, b, s.Data, bstd.MarshalString, func(n int, b []byte, v any) int {
return marshalAny(n, b, v)
})
// Time fields as Unix timestamps
n = bstd.MarshalInt64(n, b, s.CreatedAt.Unix())
n = bstd.MarshalInt64(n, b, s.UpdatedAt.Unix())
n = bstd.MarshalInt64(n, b, s.LastUsed.Unix())
n = bstd.MarshalInt64(n, b, s.Expiry.Unix())
return n
}
// UnmarshalPlain deserializes the session from binary
func (s *Session) UnmarshalPlain(n int, b []byte) (int, error) {
var err error
// ID
n, s.ID, err = bstd.UnmarshalString(n, b)
if err != nil {
return n, err
}
// Data map
n, s.Data, err = bstd.UnmarshalMap[string, any](n, b, bstd.UnmarshalString, func(n int, b []byte) (int, any, error) {
return unmarshalAny(n, b)
})
if err != nil {
return n, err
}
// Time fields as Unix timestamps
var timestamp int64
n, timestamp, err = bstd.UnmarshalInt64(n, b)
if err != nil {
return n, err
}
s.CreatedAt = time.Unix(timestamp, 0)
n, timestamp, err = bstd.UnmarshalInt64(n, b)
if err != nil {
return n, err
}
s.UpdatedAt = time.Unix(timestamp, 0)
n, timestamp, err = bstd.UnmarshalInt64(n, b)
if err != nil {
return n, err
}
s.LastUsed = time.Unix(timestamp, 0)
n, timestamp, err = bstd.UnmarshalInt64(n, b)
if err != nil {
return n, err
}
s.Expiry = time.Unix(timestamp, 0)
return n, nil
}
// Marshal serializes the session using benc
func (s *Session) Marshal() ([]byte, error) {
size := s.SizePlain()
data, err := bufPool.Marshal(size, func(b []byte) (n int) {
return s.MarshalPlain(0, b)
})
if err != nil {
return nil, err
}
return data, nil
}
// Unmarshal deserializes a session using benc
func Unmarshal(data []byte) (*Session, error) {
session := GetFromPool()
_, err := session.UnmarshalPlain(0, data)
if err != nil {
ReturnToPool(session)
return nil, err
}
return session, nil
}
// Type identifiers for any values
const (
typeNull byte = 0
typeString byte = 1
typeInt byte = 2
typeFloat byte = 3
typeBool byte = 4
typeBytes byte = 5
)
// sizeAny calculates the size needed for any value
func sizeAny(v any) int {
if v == nil {
return 1 // Just the type byte
}
// 1 byte for type + size of the value
switch val := v.(type) {
case string:
return 1 + bstd.SizeString(val)
case int:
return 1 + bstd.SizeInt64()
case int64:
return 1 + bstd.SizeInt64()
case float64:
return 1 + bstd.SizeFloat64()
case bool:
return 1 + bstd.SizeBool()
case []byte:
return 1 + bstd.SizeBytes(val)
default:
// Convert unhandled types to string
return 1 + bstd.SizeString("unknown")
}
}
// marshalAny serializes any value
func marshalAny(n int, b []byte, v any) int {
if v == nil {
b[n] = typeNull
return n + 1
}
switch val := v.(type) {
case string:
b[n] = typeString
return bstd.MarshalString(n+1, b, val)
case int:
b[n] = typeInt
return bstd.MarshalInt64(n+1, b, int64(val))
case int64:
b[n] = typeInt
return bstd.MarshalInt64(n+1, b, val)
case float64:
b[n] = typeFloat
return bstd.MarshalFloat64(n+1, b, val)
case bool:
b[n] = typeBool
return bstd.MarshalBool(n+1, b, val)
case []byte:
b[n] = typeBytes
return bstd.MarshalBytes(n+1, b, val)
default:
// Convert unhandled types to string
b[n] = typeString
return bstd.MarshalString(n+1, b, "unknown")
}
}
// unmarshalAny deserializes any value
func unmarshalAny(n int, b []byte) (int, any, error) {
if len(b) <= n {
return n, nil, benc.ErrBufTooSmall
}
typeId := b[n]
n++
switch typeId {
case typeNull:
return n, nil, nil
case typeString:
return bstd.UnmarshalString(n, b)
case typeInt:
var val int64
var err error
n, val, err = bstd.UnmarshalInt64(n, b)
return n, val, err
case typeFloat:
var val float64
var err error
n, val, err = bstd.UnmarshalFloat64(n, b)
return n, val, err
case typeBool:
var val bool
var err error
n, val, err = bstd.UnmarshalBool(n, b)
return n, val, err
case typeBytes:
return bstd.UnmarshalBytesCopied(n, b)
default:
// Unknown type, return nil
return n, nil, nil
}
} }

3
go.mod
View File

@ -5,9 +5,7 @@ go 1.24.1
require ( require (
git.sharkk.net/Sky/LuaJIT-to-Go v0.0.0 git.sharkk.net/Sky/LuaJIT-to-Go v0.0.0
github.com/VictoriaMetrics/fastcache v1.12.2 github.com/VictoriaMetrics/fastcache v1.12.2
github.com/deneonet/benc v1.1.7
github.com/goccy/go-json v0.10.5 github.com/goccy/go-json v0.10.5
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/bytebufferpool v1.0.0
github.com/valyala/fasthttp v1.60.0 github.com/valyala/fasthttp v1.60.0
) )
@ -17,7 +15,6 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
) )

12
go.sum
View File

@ -7,34 +7,22 @@ github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOL
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deneonet/benc v1.1.7 h1:0XPxTTVJZq/ulxXvMn2Mzjx5XquekVky3wX6eTgA0vA=
github.com/deneonet/benc v1.1.7/go.mod h1:UCfkM5Od0B2huwv/ZItvtUb7QnALFt9YXtX8NXX4Lts=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw=
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=