forked from SunRed/discord-tweeter
feat: Major refactor, implement web, caching, better tests and build files
* Update golang version to 1.24 * Update multiarch Dockerfile to be more ISA agnostic * Refactor existing code and properly structure project into modules * Get rid of global variables except where necessary (go:embed) * Add default values to Config * Add webserver with templates to finally correctly serve videos and gifs * Add tiny caching library to decrease api load and improve latency * Improve Webhook data preparation by filtering out redundant links from the tweet text and properly attaching videos and gifs in separate webhook request by utilising new webserver * Improve tests for filter function * Improve bake definition for easier CI integration
This commit is contained in:
parent
7562b86894
commit
21d580d1a6
24 changed files with 752 additions and 209 deletions
52
pkg/cache/cache.go
vendored
Normal file
52
pkg/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cache[K comparable, V any] struct {
|
||||
data map[K]entry[K, V]
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
type entry[K comparable, V any] struct {
|
||||
value V
|
||||
expiration time.Time
|
||||
}
|
||||
|
||||
func New[K comparable, V any]() *Cache[K, V] {
|
||||
return &Cache[K, V]{
|
||||
data: make(map[K]entry[K, V]),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.data[key] = entry[K, V]{
|
||||
value: value,
|
||||
expiration: time.Now().Add(ttl),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) Get(key K) (V, bool) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
entry, ok := c.data[key]
|
||||
if !ok || time.Now().After(entry.expiration) {
|
||||
delete(c.data, key)
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return entry.value, true
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) Delete(key K) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
delete(c.data, key)
|
||||
}
|
145
pkg/cache/cache_test.go
vendored
Normal file
145
pkg/cache/cache_test.go
vendored
Normal file
|
@ -0,0 +1,145 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCacheBasicOperations(t *testing.T) {
|
||||
cache := New[string, int]()
|
||||
|
||||
// Test Set and Get
|
||||
cache.Set("key1", 42, 10*time.Second)
|
||||
value, exists := cache.Get("key1")
|
||||
if !exists {
|
||||
t.Errorf("expected key to exist")
|
||||
}
|
||||
if value != 42 {
|
||||
t.Errorf("expected value 42, got %v", value)
|
||||
}
|
||||
|
||||
// Test non-existent key
|
||||
value, exists = cache.Get("nonexistent")
|
||||
if exists {
|
||||
t.Errorf("expected key to not exist")
|
||||
}
|
||||
if value != 0 {
|
||||
t.Errorf("expected zero value, got %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheExpiration(t *testing.T) {
|
||||
cache := New[string, int]()
|
||||
|
||||
// Set value with very short TTL
|
||||
cache.Set("short-lived", 42, 100*time.Millisecond)
|
||||
|
||||
// Verify initial existence
|
||||
value, exists := cache.Get("short-lived")
|
||||
if !exists {
|
||||
t.Errorf("expected key to exist initially")
|
||||
}
|
||||
if value != 42 {
|
||||
t.Errorf("expected value 42, got %v", value)
|
||||
}
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// Verify expiration
|
||||
value, exists = cache.Get("short-lived")
|
||||
if exists {
|
||||
t.Errorf("expected key to have expired")
|
||||
}
|
||||
if value != 0 {
|
||||
t.Errorf("expected zero value, got %v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheConcurrentAccess(t *testing.T) {
|
||||
cache := New[string, int]()
|
||||
|
||||
// Start multiple writers
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
cache.Set(fmt.Sprintf("key%d", i), i*2, 10*time.Second)
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Start multiple readers
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
cache.Get(fmt.Sprintf("key%d", i))
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify all values were written correctly
|
||||
for i := 0; i < 10; i++ {
|
||||
value, exists := cache.Get(fmt.Sprintf("key%d", i))
|
||||
if !exists {
|
||||
t.Errorf("expected key%d to exist", i)
|
||||
}
|
||||
if value != (i * 2) {
|
||||
t.Errorf("expected value %d for key%d, got %d", i*2, i, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheGenericTypes(t *testing.T) {
|
||||
// Test with string values
|
||||
strCache := New[int, string]()
|
||||
strCache.Set(1, "hello", 10*time.Second)
|
||||
value, exists := strCache.Get(1)
|
||||
if !exists {
|
||||
t.Errorf("expected key to exist")
|
||||
}
|
||||
if value != "hello" {
|
||||
t.Errorf("expected value hello, got %v", value)
|
||||
}
|
||||
|
||||
// Test with struct values
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
personCache := New[string, Person]()
|
||||
personCache.Set("john", Person{"John", 30}, 10*time.Second)
|
||||
person, exists := personCache.Get("john")
|
||||
if !exists {
|
||||
t.Errorf("expected key to exist")
|
||||
}
|
||||
expected := Person{"John", 30}
|
||||
if !reflect.DeepEqual(person, expected) {
|
||||
t.Errorf("expected %+v, got %+v", expected, person)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheDelete(t *testing.T) {
|
||||
cache := New[string, int]()
|
||||
|
||||
// Set and verify
|
||||
cache.Set("key1", 42, 10*time.Second)
|
||||
_, exists := cache.Get("key1")
|
||||
if !exists {
|
||||
t.Errorf("expected key to exist")
|
||||
}
|
||||
|
||||
// Delete
|
||||
cache.Delete("key1")
|
||||
|
||||
// Verify deletion
|
||||
_, exists = cache.Get("key1")
|
||||
if exists {
|
||||
t.Errorf("expected key to not exist after deletion")
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue