Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,29 @@ func main() {
}
```

All unexpired items can be iterated over with the `Range` and
`RangeBackwards` methods, or, on Go 1.23 and above, with the
`KeysSeq` and `ItemsSeq` range-over-func iterators. The latter are the
lazy counterparts of `Keys` and `Items`: they visit items in the same
order as `Range` (from the most to the least recently added or updated)
without allocating an intermediate slice or map, and support stopping
early with `break`:
```go
func main() {
cache := ttlcache.New[string, string]()
cache.Set("first", "value1", ttlcache.DefaultTTL)
cache.Set("second", "value2", ttlcache.DefaultTTL)

for key := range cache.KeysSeq() {
fmt.Println(key)
}

for key, item := range cache.ItemsSeq() {
fmt.Println(key, item.Value())
}
}
```

## Examples
See the [examples](https://github.com/jellydator/ttlcache/tree/v3/examples)
directory for complete applications demonstrating how to use `ttlcache`.
Expand Down
33 changes: 33 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"container/list"
"context"
"fmt"
"iter"
"sync"
"time"

Expand Down Expand Up @@ -641,6 +642,38 @@ func (c *Cache[K, V]) RangeBackwards(fn func(item *Item[K, V]) bool) {
c.items.mu.RUnlock()
}

// KeysSeq returns an iterator that yields the key of each unexpired item
// in the cache. It is the lazy, range-over-func counterpart of Keys and
// visits items in the same order as Range (from the most to the least
// recently added or updated). Stopping the iteration early is supported.
//
// As with Range, the cache lock is not held while a key is yielded, so it
// is safe to call other cache methods from within the loop.
func (c *Cache[K, V]) KeysSeq() iter.Seq[K] {
return func(yield func(K) bool) {
c.Range(func(item *Item[K, V]) bool {
return yield(item.Key())
})
}
}

// ItemsSeq returns an iterator that yields the key and the item of each
// unexpired item in the cache. It is the lazy, range-over-func counterpart
// of Items and visits items in the same order as Range (from the most to
// the least recently added or updated). Stopping the iteration early is
// supported.
//
// As with Range, the cache lock is not held while an item is yielded, so it
// is safe to call other cache methods from within the loop. Unlike Items, it
// does not allocate an intermediate map.
func (c *Cache[K, V]) ItemsSeq() iter.Seq2[K, *Item[K, V]] {
return func(yield func(K, *Item[K, V]) bool) {
c.Range(func(item *Item[K, V]) bool {
return yield(item.Key(), item)
})
}
}

// Metrics returns the metrics of the cache.
func (c *Cache[K, V]) Metrics() Metrics {
c.metricsMu.RLock()
Expand Down
81 changes: 81 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,87 @@ func Test_Cache_RangeBackwards(t *testing.T) {
})
}

func Test_Cache_KeysSeq(t *testing.T) {
c := prepCache(0, DefaultTTL, "1", "2", "3", "4", "5")
addExpiredCacheItems(c, "6")

// full iteration, expired items excluded, order matches Range
var results []string
for key := range c.KeysSeq() {
results = append(results, key)
}
assert.Equal(t, []string{"5", "4", "3", "2", "1"}, results)

// early stop via break
results = nil
for key := range c.KeysSeq() {
results = append(results, key)
if key == "4" {
break
}
}
assert.Equal(t, []string{"5", "4"}, results)

// empty cache does not panic and yields nothing
emptyCache := New[string, string]()
assert.NotPanics(t, func() {
for range emptyCache.KeysSeq() {
t.Fatal("empty cache must not yield any keys")
}
})

// calling other cache methods during iteration is safe
deletedCache := New[string, string]()
addTTLCacheItems(deletedCache, time.Minute, "6", "3", "4")
assert.NotPanics(t, func() {
for range deletedCache.KeysSeq() {
deletedCache.DeleteAll()
}
})
}

func Test_Cache_ItemsSeq(t *testing.T) {
c := prepCache(0, DefaultTTL, "1", "2", "3", "4", "5")
addExpiredCacheItems(c, "6")

// full iteration, expired items excluded, order matches Range,
// and the yielded key matches the item's own key
var results []string
for key, item := range c.ItemsSeq() {
require.NotNil(t, item)
assert.Equal(t, key, item.Key())
results = append(results, key)
}
assert.Equal(t, []string{"5", "4", "3", "2", "1"}, results)

// early stop via break
results = nil
for key := range c.ItemsSeq() {
results = append(results, key)
if key == "4" {
break
}
}
assert.Equal(t, []string{"5", "4"}, results)

// empty cache does not panic and yields nothing
emptyCache := New[string, string]()
assert.NotPanics(t, func() {
for range emptyCache.ItemsSeq() {
t.Fatal("empty cache must not yield any items")
}
})

// calling other cache methods during iteration is safe
deletedCache := New[string, string]()
addTTLCacheItems(deletedCache, time.Minute, "6", "3", "4")
assert.NotPanics(t, func() {
for range deletedCache.ItemsSeq() {
deletedCache.DeleteAll()
}
})
}

func Test_Cache_Metrics(t *testing.T) {
cache := Cache[string, string]{
metrics: Metrics{Evictions: 10},
Expand Down
Loading