initial commit
Marco Andronaco andronacomarco@gmail.com
Thu, 10 Oct 2024 01:55:49 +0200
A
LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT) + +Copyright (c) 2024 BiRabittoh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
A
README
@@ -0,0 +1,44 @@
+myks +---- + +A barebones in-memory keystore in 125~ lines of actual code. + + +FEATURES + +• Set key-value pairs with a given expiration. +• Set key-value pairs without a given expiration. +• Get values of any type (uses Go generics). +• Delete unwanted values. +• Probably not memory-safe! + + +INSTALLING + +`go get github.com/BiRabittoh/myks` + + +USAGE + +`ks := myks.New[string](5 * time.Minute)` creates a new instance and +starts a goroutine that cleans up expired entries every 5 minutes. +You can also pass `0` to disable the cleanup goroutine altogether. + +`go ks.StartCleanup(10 * time.Minute)` starts the cleanup goroutine +at a later stage or changes its interval, if it's already started. + +`ks.Set("key", "value", 2 * time.Minute)` sets a new key-value pair +with an expiration of 2 minutes. +You can also pass `0` to keep that value until the app is stopped. + +`ks.Get("key")` returns a pointer to the set value and an error +value (either nil, `ks.ErrNotFound` or `ks.ErrExpired`). + +`ks.Delete("key")` deletes a given key-value pair. + +`ks.Keys()` returns an iterator for all keys that are not expired. + + +LICENSE + +myks is licensed under MIT.
A
myks.go
@@ -0,0 +1,138 @@
+package myks + +import ( + "errors" + "iter" + "maps" + "sync" + "time" +) + +var ( + ErrNotFound = errors.New("entry was not found") + ErrExpired = errors.New("entry is expired") +) + +// KeyStore is the main structure of this module +type KeyStore[T any] struct { + mu sync.RWMutex + data map[string]entry[T] + stopChan chan struct{} + cleanupInterval time.Duration +} + +// entry is a keystore value, with an optional expiration +type entry[T any] struct { + value T + expiration *time.Time +} + +// New creates a new keystore with an optional goroutine to automatically clean expired values +func New[T any](cleanupInterval time.Duration) *KeyStore[T] { + ks := &KeyStore[T]{ // do not set cleanupInterval here + data: make(map[string]entry[T]), + stopChan: make(chan struct{}), + } + + go ks.StartCleanup(cleanupInterval) + + return ks +} + +// Set saves a key-value pair with a given expiration. +// If duration <= 0, the entry will never expire. +func (ks *KeyStore[T]) Set(key string, value T, duration time.Duration) { + ks.mu.Lock() + defer ks.mu.Unlock() + + var expiration *time.Time + if duration > 0 { + exp := time.Now().Add(duration) + expiration = &exp + } + + ks.data[key] = entry[T]{ + value: value, + expiration: expiration, + } +} + +// Get returns the value for the given key if it exists and it's not expired. +func (ks *KeyStore[T]) Get(key string) (*T, error) { + ks.mu.Lock() + defer ks.mu.Unlock() + + ent, exists := ks.data[key] + if !exists { + return nil, ErrNotFound + } + + if ent.expiration != nil && ent.expiration.Before(time.Now()) { + delete(ks.data, key) + return nil, ErrExpired + } + + return &ent.value, nil +} + +// Delete removes a key-value pair from the KeyStore. +func (ks *KeyStore[T]) Delete(key string) { + ks.mu.Lock() + defer ks.mu.Unlock() + + delete(ks.data, key) +} + +// Keys returns an iterator for all not-expired keys in the KeyStore. +func (ks *KeyStore[T]) Keys() iter.Seq[string] { + ks.Clean() + + ks.mu.RLock() + defer ks.mu.RUnlock() + return maps.Keys(ks.data) +} + +// Clean deletes all expired keys from the keystore. +func (ks *KeyStore[T]) Clean() { + now := time.Now() + ks.mu.Lock() + defer ks.mu.Unlock() + + for k, ent := range ks.data { + if ent.expiration != nil && ent.expiration.Before(now) { + delete(ks.data, k) + } + } +} + +// StartCleanup starts a goroutine that periodically deletes all expired keys from the keystore. +func (ks *KeyStore[T]) StartCleanup(cleanupInterval time.Duration) { + if cleanupInterval == 0 { + return + } + + ks.StopCleanup() + ks.cleanupInterval = cleanupInterval + + ticker := time.NewTicker(ks.cleanupInterval) + for { + select { + case <-ticker.C: + ks.Clean() + case <-ks.stopChan: + ticker.Stop() + return + } + } +} + +// StopCleanup stops the cleanup goroutine. +func (ks *KeyStore[T]) StopCleanup() { + ks.mu.Lock() + defer ks.mu.Unlock() + + if ks.cleanupInterval != 0 { + close(ks.stopChan) + ks.cleanupInterval = 0 + } +}