Documentation
¶
Overview ¶
Package kv keeps an application's current state in kv.db inside a tinystore.Store: buckets of one value type and counters, keyed by text in branches, with expiry by the store's clock and versions that never repeat.
state, err := kv.Open(ctx, store, kv.Options{})
sessions, err := kv.OpenBucket[Session](ctx, state, "sessions")
err = sessions.Of(userID).Set(ctx, token, session, kv.TTL(24*time.Hour))
session, found, err := sessions.Of(userID).Get(ctx, token)
A write waits for the file's one writer beside the writes of other goroutines and commits with them, one fsync for all; a read is one statement. Start with kv.go and bucket.go; every other file holds one step:
kv.go Open, Close, Snapshot: the handle, its revision and admission bucket.go OpenBucket, Of, WithTx: a handle on a bucket of values counters.go OpenCounters, Add, Max, Get, Delete: a handle on counters memory.go what LoseAtMost counters hold between flushes, and the flush clear.go Clear: a branch deleted at once, or marked and deleted by Maintain branch.go what both handles share: a branch's path and where its calls run options.go the engine's bounds, the options of buckets, counters and calls keys.go a key's text and the path owners and a key make values.go a value's bytes by its type, and where they are kept entry.go Entry, Version, Query, Page, KeyError read.go Get, GetEntry, Has, Scan, All renew.go what Sliding reads ask for, and the flush that writes it write.go Set, SetEntry, SetIfAbsent, SetEntryIfAbsent, Take, Delete, Touch tx.go Tx and View: several calls in one transaction or snapshot expiry.go Maintain: expired keys and the values they spilled
Example (AttemptLimits) ¶
Attempts are counted in memory, fifteen minutes from the first; a crash may forget the last second of them, which a limit can afford.
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/tinyshed/tinystore"
"github.com/tinyshed/tinystore/kv"
)
// clock is the store's clock in the examples, moved rather than waited for
type clock struct{ now time.Time }
func (c *clock) Now() time.Time { return c.now }
func (c *clock) advance(d time.Duration) { c.now = c.now.Add(d) }
// exampleState opens kv in a Manual store in a new directory, so that an
// example moves its clock instead of sleeping; done closes and removes it
func exampleState() (state *kv.Store, at *clock, done func()) {
ctx := context.Background()
directory, err := os.MkdirTemp("", "tinystore-kv-")
check(err)
at = &clock{now: time.Date(2026, 9, 26, 12, 0, 0, 0, time.UTC)}
store, err := tinystore.Open(ctx, directory, tinystore.Options{Manual: true, Clock: at.Now})
check(err)
state, err = kv.Open(ctx, store, kv.Options{})
check(err)
return state, at, func() {
_ = store.Close(ctx)
_ = os.RemoveAll(directory)
}
}
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
ctx := context.Background()
state, clock, done := exampleState()
defer done()
attempts, err := kv.OpenCounters(ctx, state, "login-attempts",
kv.DefaultTTL(15*time.Minute), kv.LoseAtMost(time.Second))
check(err)
refused := func(ip string) bool {
n, addErr := attempts.Of("ip").Add(ctx, ip, 1)
check(addErr)
return n > 3
}
var answers []bool
for range 4 {
answers = append(answers, refused("10.0.0.1"))
}
fmt.Println(answers)
clock.advance(15 * time.Minute)
fmt.Println(refused("10.0.0.1"))
}
Output: [false false false true] false
Example (Drafts) ¶
Two tabs edit one draft: a save goes through only if nobody saved since the tab read it.
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/tinyshed/tinystore"
"github.com/tinyshed/tinystore/kv"
)
// clock is the store's clock in the examples, moved rather than waited for
type clock struct{ now time.Time }
func (c *clock) Now() time.Time { return c.now }
func (c *clock) advance(d time.Duration) { c.now = c.now.Add(d) }
// exampleState opens kv in a Manual store in a new directory, so that an
// example moves its clock instead of sleeping; done closes and removes it
func exampleState() (state *kv.Store, at *clock, done func()) {
ctx := context.Background()
directory, err := os.MkdirTemp("", "tinystore-kv-")
check(err)
at = &clock{now: time.Date(2026, 9, 26, 12, 0, 0, 0, time.UTC)}
store, err := tinystore.Open(ctx, directory, tinystore.Options{Manual: true, Clock: at.Now})
check(err)
state, err = kv.Open(ctx, store, kv.Options{})
check(err)
return state, at, func() {
_ = store.Close(ctx)
_ = os.RemoveAll(directory)
}
}
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
ctx := context.Background()
state, _, done := exampleState()
defer done()
drafts, err := kv.OpenBucket[string](ctx, state, "drafts")
check(err)
check(drafts.Of(42).Set(ctx, "note-1", "milk"))
first, _, err := drafts.Of(42).GetEntry(ctx, "note-1")
check(err)
second, _, err := drafts.Of(42).GetEntry(ctx, "note-1")
check(err)
_, err = drafts.Of(42).SetEntry(ctx, "note-1", "milk, bread", kv.IfVersion(first.Version))
fmt.Println(err)
_, err = drafts.Of(42).SetEntry(ctx, "note-1", "milk, eggs", kv.IfVersion(second.Version))
fmt.Println(errors.Is(err, tinystore.ErrConflict))
}
Output: <nil> true
Example (Sessions) ¶
Sessions live thirty days from their last request and are kept by a digest of their token, so that a copy of kv.db signs nobody in. A token is rotated in one write, a user sees where they are signed in, and signs out everywhere at once.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"time"
"github.com/tinyshed/tinystore"
"github.com/tinyshed/tinystore/kv"
)
// clock is the store's clock in the examples, moved rather than waited for
type clock struct{ now time.Time }
func (c *clock) Now() time.Time { return c.now }
func (c *clock) advance(d time.Duration) { c.now = c.now.Add(d) }
// exampleState opens kv in a Manual store in a new directory, so that an
// example moves its clock instead of sleeping; done closes and removes it
func exampleState() (state *kv.Store, at *clock, done func()) {
ctx := context.Background()
directory, err := os.MkdirTemp("", "tinystore-kv-")
check(err)
at = &clock{now: time.Date(2026, 9, 26, 12, 0, 0, 0, time.UTC)}
store, err := tinystore.Open(ctx, directory, tinystore.Options{Manual: true, Clock: at.Now})
check(err)
state, err = kv.Open(ctx, store, kv.Options{})
check(err)
return state, at, func() {
_ = store.Close(ctx)
_ = os.RemoveAll(directory)
}
}
func check(err error) {
if err != nil {
panic(err)
}
}
type Session struct {
Device string
}
func main() {
ctx := context.Background()
state, clock, done := exampleState()
defer done()
sessions, err := kv.OpenBucket[Session](ctx, state, "sessions", kv.Sliding(30*24*time.Hour))
check(err)
check(sessions.Of(42).Set(ctx, digest("t1"), Session{Device: "phone"}))
check(sessions.Of(42).Set(ctx, digest("t2"), Session{Device: "laptop"}))
clock.advance(20 * 24 * time.Hour)
s, found, err := sessions.Of(42).Get(ctx, digest("t1")) // and thirty more days from now
check(err)
fmt.Println(s.Device, found)
// after a password change: a new token in the old one's place, in one write
check(state.Tx(ctx, func(tx *kv.Tx) error {
session, there, takeErr := sessions.WithTx(tx).Of(42).Take(ctx, digest("t1"))
if takeErr != nil || !there {
return takeErr
}
return sessions.WithTx(tx).Of(42).Set(ctx, digest("t3"), session)
}))
_, found, err = sessions.Of(42).Get(ctx, digest("t1"))
check(err)
fmt.Println("the old token signs in:", found)
for entry, err := range sessions.Of(42).All(ctx) {
check(err)
fmt.Println("signed in:", entry.Value.Device)
}
check(sessions.Of(42).Clear(ctx))
_, found, err = sessions.Of(42).Get(ctx, digest("t3"))
check(err)
fmt.Println(found)
}
func digest(code string) string {
sum := sha256.Sum256([]byte(code))
return hex.EncodeToString(sum[:])
}
Output: phone true the old token signs in: false signed in: phone signed in: laptop false
Example (SignInLink) ¶
A sign-in link works once, for fifteen minutes.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"time"
"github.com/tinyshed/tinystore"
"github.com/tinyshed/tinystore/kv"
)
// clock is the store's clock in the examples, moved rather than waited for
type clock struct{ now time.Time }
func (c *clock) Now() time.Time { return c.now }
func (c *clock) advance(d time.Duration) { c.now = c.now.Add(d) }
// exampleState opens kv in a Manual store in a new directory, so that an
// example moves its clock instead of sleeping; done closes and removes it
func exampleState() (state *kv.Store, at *clock, done func()) {
ctx := context.Background()
directory, err := os.MkdirTemp("", "tinystore-kv-")
check(err)
at = &clock{now: time.Date(2026, 9, 26, 12, 0, 0, 0, time.UTC)}
store, err := tinystore.Open(ctx, directory, tinystore.Options{Manual: true, Clock: at.Now})
check(err)
state, err = kv.Open(ctx, store, kv.Options{})
check(err)
return state, at, func() {
_ = store.Close(ctx)
_ = os.RemoveAll(directory)
}
}
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
ctx := context.Background()
state, clock, done := exampleState()
defer done()
codes, err := kv.OpenBucket[int64](ctx, state, "login-codes", kv.DefaultTTL(15*time.Minute))
check(err)
check(codes.Set(ctx, digest("7Q2X"), 42)) // a digest, so a copy of the file holds no working link
userID, found, err := codes.Take(ctx, digest("7Q2X"))
check(err)
fmt.Println(userID, found)
_, found, err = codes.Take(ctx, digest("7Q2X")) // the second click
check(err)
fmt.Println(found)
check(codes.Set(ctx, digest("K9ZP"), 43))
clock.advance(16 * time.Minute)
_, found, err = codes.Take(ctx, digest("K9ZP")) // too late
check(err)
fmt.Println(found)
}
func digest(code string) string {
sum := sha256.Sum256([]byte(code))
return hex.EncodeToString(sum[:])
}
Output: 42 true false false
Example (WebhookClaims) ¶
A provider delivers an event until it gets a 200, so an event may arrive twice. A claim's version keeps a handler slower than its claim from finishing the next one's.
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/tinyshed/tinystore"
"github.com/tinyshed/tinystore/kv"
)
// clock is the store's clock in the examples, moved rather than waited for
type clock struct{ now time.Time }
func (c *clock) Now() time.Time { return c.now }
func (c *clock) advance(d time.Duration) { c.now = c.now.Add(d) }
// exampleState opens kv in a Manual store in a new directory, so that an
// example moves its clock instead of sleeping; done closes and removes it
func exampleState() (state *kv.Store, at *clock, done func()) {
ctx := context.Background()
directory, err := os.MkdirTemp("", "tinystore-kv-")
check(err)
at = &clock{now: time.Date(2026, 9, 26, 12, 0, 0, 0, time.UTC)}
store, err := tinystore.Open(ctx, directory, tinystore.Options{Manual: true, Clock: at.Now})
check(err)
state, err = kv.Open(ctx, store, kv.Options{})
check(err)
return state, at, func() {
_ = store.Close(ctx)
_ = os.RemoveAll(directory)
}
}
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
ctx := context.Background()
state, clock, done := exampleState()
defer done()
seen, err := kv.OpenBucket[struct{}](ctx, state, "stripe-events")
check(err)
handle := func(eventID string) string {
claim, first, claimErr := seen.SetEntryIfAbsent(ctx, eventID, struct{}{}, kv.TTL(10*time.Minute))
check(claimErr)
if !first {
return "handled before, or being handled now"
}
check(seen.Set(ctx, eventID, struct{}{}, kv.IfVersion(claim.Version), kv.TTL(7*24*time.Hour)))
return "handled"
}
fmt.Println(handle("evt_1"))
fmt.Println(handle("evt_1"))
slow, _, err := seen.SetEntryIfAbsent(ctx, "evt_2", struct{}{}, kv.TTL(10*time.Minute))
check(err)
clock.advance(11 * time.Minute)
fmt.Println(handle("evt_2"))
err = seen.Set(ctx, "evt_2", struct{}{}, kv.IfVersion(slow.Version))
fmt.Println(errors.Is(err, tinystore.ErrConflict))
}
Output: handled handled before, or being handled now handled true
Index ¶
- Variables
- type Allowance
- type Bucket
- func (b *Bucket[V]) All(ctx context.Context) iter.Seq2[Entry[V], error]
- func (b *Bucket[V]) Clear(ctx context.Context) error
- func (b *Bucket[V]) Delete(ctx context.Context, key any, options ...Option) error
- func (b *Bucket[V]) Get(ctx context.Context, key any) (V, bool, error)
- func (b *Bucket[V]) GetEntry(ctx context.Context, key any) (Entry[V], bool, error)
- func (b *Bucket[V]) Has(ctx context.Context, key any) (bool, error)
- func (b *Bucket[V]) Of(owners ...any) *Bucket[V]
- func (b *Bucket[V]) Scan(ctx context.Context, query Query) (Page[V], error)
- func (b *Bucket[V]) Set(ctx context.Context, key any, value V, options ...Option) error
- func (b *Bucket[V]) SetEntry(ctx context.Context, key any, value V, options ...Option) (Entry[V], error)
- func (b *Bucket[V]) SetEntryIfAbsent(ctx context.Context, key any, value V, options ...Option) (Entry[V], bool, error)
- func (b *Bucket[V]) SetIfAbsent(ctx context.Context, key any, value V, options ...Option) (bool, error)
- func (b *Bucket[V]) Take(ctx context.Context, key any, options ...Option) (V, bool, error)
- func (b *Bucket[V]) Touch(ctx context.Context, key any, options ...Option) (bool, error)
- func (b *Bucket[V]) WithTx(tx *Tx) *Bucket[V]
- type BucketOption
- type Codec
- type Config
- type ConfigOption
- type CounterOption
- type Counters
- func (c *Counters) Add(ctx context.Context, key any, n int64) (int64, error)
- func (c *Counters) Clear(ctx context.Context) error
- func (c *Counters) Delete(ctx context.Context, key any) error
- func (c *Counters) Get(ctx context.Context, key any) (int64, error)
- func (c *Counters) Max(ctx context.Context, key any, n int64) (int64, error)
- func (c *Counters) Of(owners ...any) *Counters
- func (c *Counters) WithTx(tx *Tx) *Counters
- type Entry
- type KeyError
- type Limiter
- type LimiterOption
- type Maintenance
- type Once
- type OpenOption
- type Option
- type Options
- type Page
- type Query
- type Quota
- func (q *Quota) Allow(ctx context.Context, key any) (QuotaUsage, error)
- func (q *Quota) AllowN(ctx context.Context, key any, n int64) (QuotaUsage, error)
- func (q *Quota) Delete(ctx context.Context, key any) error
- func (q *Quota) Get(ctx context.Context, key any) (QuotaUsage, error)
- func (q *Quota) Of(owners ...any) *Quota
- func (q *Quota) Refund(ctx context.Context, key any) error
- func (q *Quota) RefundN(ctx context.Context, key any, n int64) error
- type QuotaOption
- type QuotaUsage
- type Raw
- type RawConfig
- type RawKind
- type Source
- type Store
- func (s *Store) Close(ctx context.Context) error
- func (s *Store) Maintain(ctx context.Context) (Maintenance, error)
- func (s *Store) Snapshot(ctx context.Context, dir string) ([]tinystore.SnapshotFile, error)
- func (s *Store) Tx(ctx context.Context, work func(*Tx) error) error
- func (s *Store) View(ctx context.Context, read func(*Tx) error) error
- type Tx
- type Version
- type WindowUsage
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrOutcomeUnknown = sqlite.ErrOutcomeUnknown
ErrOutcomeUnknown is returned for a write whose group's commit failed. The write may or may not be in the file, so its caller reads it back before writing again.
Functions ¶
This section is empty.
Types ¶
type Allowance ¶
type Allowance struct {
OK bool
// Left is how many more requests would pass now.
Left int64
// RetryAfter is how long until the request would pass; zero when OK.
RetryAfter time.Duration
}
Allowance is what a limiter answers a request.
type Bucket ¶
type Bucket[V any] struct { // contains filtered or unexported fields }
Bucket holds values of type V by key. Of and WithTx give handles on the same bucket, and a handle may be used from any number of goroutines.
func OpenBucket ¶
func OpenBucket[V any](ctx context.Context, state *Store, name string, options ...BucketOption) (*Bucket[V], error)
OpenBucket opens the bucket name of kv.db for values of type V, creating it the first time. A name that holds counters is ErrInvalid.
func (*Bucket[V]) All ¶
All is every one of this branch's own keys, in Scan's order, a page of Scan at a time. Each page is its own snapshot and none is held between pages, so a slow loop keeps no reader open, and a key written or deleted during the walk may or may not be met; inside View or Tx the pages share its snapshot. An error ends the walk as its last element.
func (*Bucket[V]) Clear ¶
Clear removes every key of this branch and of the branches under it. A branch of up to 10,000 keys is deleted in one transaction; a larger one is marked cleared at the file's revision, which hides its keys from every call at once, and Maintain deletes them 10,000 a transaction. A key written after the Clear is a new key. Inside Tx a branch over 10,000 keys is ErrLimit, since deleting it would hold the writer for seconds.
func (*Bucket[V]) GetEntry ¶
GetEntry is Get with the key's version and expiry, the expiry as the file has it: a Sliding renewal waiting for its flush is not in it yet.
func (*Bucket[V]) Of ¶
Of is the branch of this bucket that owners name, one after another: a string, a []byte or an integer each, an integer by its decimal text. A branch exists while it holds keys; there is nothing to create or drop.
func (*Bucket[V]) Scan ¶
Scan is a page of this branch's own keys, not those of the branches under it, from one snapshot, in the byte order of their text: "10" before "9".
func (*Bucket[V]) Set ¶
Set writes value under key. A key it creates, or one that had expired, takes the bucket's DefaultTTL unless kv.TTL or kv.ExpireAt says otherwise; a live key keeps the expiry it has unless one of them does.
func (*Bucket[V]) SetEntry ¶
func (b *Bucket[V]) SetEntry(ctx context.Context, key any, value V, options ...Option) (Entry[V], error)
SetEntry is Set, returning what it wrote with its new version.
func (*Bucket[V]) SetEntryIfAbsent ¶
func (b *Bucket[V]) SetEntryIfAbsent(ctx context.Context, key any, value V, options ...Option) ( Entry[V], bool, error, )
SetEntryIfAbsent is SetIfAbsent, returning the entry it wrote or the one that was there, each with its version.
func (*Bucket[V]) SetIfAbsent ¶
func (b *Bucket[V]) SetIfAbsent(ctx context.Context, key any, value V, options ...Option) (bool, error)
SetIfAbsent writes value under key only if no live key is there, and says whether it did; an expired key is absent.
func (*Bucket[V]) Take ¶
Take reads the value under key and deletes it in one step, so that of the callers taking one key only one gets it. A value that no longer decodes is ErrCorrupt and stays where it was, inside Tx as well.
type BucketOption ¶
type BucketOption interface {
// contains filtered or unexported methods
}
BucketOption changes how a bucket serves its values; none changes what its bytes mean, so a program may change them between runs.
func Sliding ¶
func Sliding(term time.Duration) BucketOption
Sliding gives a key term from the last time it was read: a key created without kv.TTL or kv.ExpireAt gets term, and a Get, GetEntry or Has renews a live key to term from now once a thirtieth of the term has passed since it last did. With DefaultTTL it is ErrInvalid.
A read does not write: the renewal waits a second for the next flush, and a crash forgets the renewals since the last. So a key read at t lives at least until t + term − term/30 unless it is renewed.
A key with less than a minute left is renewed before its read returns.
func WithCodec ¶
func WithCodec[V any](codec Codec[V]) BucketOption
WithCodec writes a bucket's values through codec instead of the bytes their type would get; a codec of another value type is ErrInvalid at OpenBucket.
type Codec ¶
Codec writes a bucket's values when the bytes their type would get are not the ones wanted; see WithCodec.
type Config ¶
type Config[T any] struct { // contains filtered or unexported fields }
Config is an application's settings, a value of T made of layers in the order given, each over the one before it, and what Update kept over them all:
kv.Defaults(Config{Port: 8080}), kv.Defaults(fromYAML)
kv.FromEnv("APP", ".env") APP_PORT=3000
config.Update(ctx, func(c *Config) { c.Port = 4000 })
Get reads it from memory. Update checks a change and keeps it field by field in kv.db, so that it outlives a restart, and every handle on the config sees it at once, in this process and through the server; Reset gives a field back to the layers under it.
T is a struct. A field's path is its JSON name, a nested struct's below its own: limits.rps. A field tagged `env:"NAME"` reads that variable; another reads the prefix and its path in upper snake case, APP_LIMITS_RPS. A field tagged `secret:"true"` comes from the layers alone: Update refuses it and Sources hides it. A field tagged `required:"true"` is ErrInvalid at OpenConfig while the layers leave it its zero value.
func OpenConfig ¶
func OpenConfig[T any](ctx context.Context, state *Store, name string, options ...ConfigOption) (*Config[T], error)
OpenConfig opens the config name of kv.db, creating it the first time, and makes it from its layers. Defaults and an environment that do not fit T, or that fail Validate, are ErrInvalid; a kept value that no longer fits its field is left out, logged, and named by Sources.
func (*Config[T]) Get ¶
func (c *Config[T]) Get() T
Get is the config now, built once a change and shared: it is not to be changed, which Update does.
func (*Config[T]) Reset ¶
Reset forgets what Update kept for paths, each a field or a struct of fields, so that they take their values from the layers under it again; without paths it forgets everything kept.
func (*Config[T]) Sources ¶
Sources says where each field's value came from, in the order of T's fields, and why a kept value is left out.
type ConfigOption ¶
type ConfigOption func(*configSettings)
ConfigOption is where a config's values come from, and what checks them.
func Defaults ¶
func Defaults(values any) ConfigOption
Defaults is a layer of values: a T, which sets every field, or a map, as a JSON or YAML library reads a file into one, which sets the fields it names. A duration may be text, "30s".
func FromEnv ¶
func FromEnv(prefix string, files ...string) ConfigOption
FromEnv is a layer of the environment: each field from its variable, files first, as a dotenv library reads them, and the process's own over them. A file that is not there is skipped.
PORT=3000 ORIGINS=a.com,b.com TIMEOUT=1h30m LIMITS={"rps":5}
func Validate ¶
func Validate[T any](check func(T) error) ConfigOption
Validate checks the config each time it is made, at open and before Update keeps a change: a change that fails is refused, and a kept value that fails is left out.
type CounterOption ¶
type CounterOption interface {
// contains filtered or unexported methods
}
func LoseAtMost ¶
func LoseAtMost(d time.Duration) CounterOption
LoseAtMost keeps the counters in memory and writes what changed every d, on Close and on Maintain, so that an Add takes no write.
A crash loses the changes since the last flush that committed, a Delete's as an Add's. A flush waits for the writer as any write does, so on a busy file the loss can reach d plus the flush's own wait and time.
Handles on counters of one name share the memory and join no transaction. Opening them again with another d, or without LoseAtMost, is ErrInvalid.
type Counters ¶
type Counters struct {
// contains filtered or unexported fields
}
Counters holds an int64 by key. Of and WithTx give handles on the same counters, and a handle may be used from any number of goroutines.
func OpenCounters ¶
func OpenCounters(ctx context.Context, state *Store, name string, options ...CounterOption) (*Counters, error)
OpenCounters opens the counters name of kv.db, creating them the first time. A name that holds values is ErrInvalid.
func (*Counters) Add ¶
Add adds n to the counter under key and returns what it holds now. An absent or expired counter starts from zero with the DefaultTTL of the counters, and a live one keeps its expiry, so a window does not slide. A sum past the int64 range is ErrLimit and changes nothing.
func (*Counters) Clear ¶
Clear removes every counter of this branch and of the branches under it, as Bucket.Clear does. LoseAtMost counters it also removes from memory once it commits, so that no flush writes them again; a Clear that fails leaves them.
func (*Counters) Max ¶
Max keeps the larger of the counter under key and n, an absent counter counting as zero, and returns what it holds now.
type Entry ¶
type Entry[V any] struct { // Key is the key's text, without the branch's owners. Key string Value V Version Version // ExpiresAt is zero for a key that never expires. ExpiresAt time.Time }
Entry is a value with what the file knows of it.
type KeyError ¶
KeyError is a call refused because of one key: its bucket and the path of owners and key it named. errors.Is finds the store's sentinel in it.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter lets each key's requests through at a steady rate with room for a burst. It is the generic cell rate algorithm: a key holds one time, when its next request is due, so there is no window whose edge lets twice the rate through, and a key costs one int64.
Rate(3, time.Second), Burst(3): three pass at once, the fourth waits 333ms
Its times live in memory and reach the file every second and on Close, as LoseAtMost counters do: a crash forgets at most the last second, and so lets at most one burst more through. A key whose time has come is as one never seen, and maintenance deletes it.
func OpenLimiter ¶
func OpenLimiter(ctx context.Context, state *Store, name string, options ...LimiterOption) (*Limiter, error)
OpenLimiter opens the limiter name of kv.db, creating it the first time. A name that holds values or counters is ErrInvalid; the rate is the caller's, and may change between runs.
type LimiterOption ¶
type LimiterOption func(*limiterSettings)
LimiterOption says how fast a limiter lets requests through.
func Burst ¶
func Burst(n int64) LimiterOption
Burst is how many requests may pass at once before the rate holds the next; count of the Rate when it is not given.
type Maintenance ¶
type Maintenance struct {
Flushed int // LoseAtMost counters written
Renewed int // Sliding keys renewed
Cleared int // rows a marked Clear hid, deleted
Expired int
}
Maintenance is what one Maintain call did.
type Once ¶
type Once[V any] struct { // contains filtered or unexported fields }
Once runs a function at most once a key and keeps what it returns, so that a request sent again is answered as the first was rather than done twice: a charge, an import, a webhook's effect.
charges.Run(ctx, "req-7", charge) → runs charge, keeps its receipt a day charges.Run(ctx, "req-7", charge) → that receipt; charge does not run
One Run of a key runs its function at a time in the store's process, the clients of its server included: a Run of a key another Run is running waits for it and returns its answer. A function's error keeps nothing, so the next Run runs it again; an answer a program wants kept, a card declined, is a value rather than an error.
A claim on a key lives in memory as long as its Run. A process that dies after a function's effect outside the store and before its answer was kept runs it again on the next Run, so such an effect carries the key too: a payment provider's own idempotency key.
func OpenOnce ¶
func OpenOnce[V any](ctx context.Context, state *Store, name string, options ...BucketOption) (*Once[V], error)
OpenOnce opens the answers name of kv.db, creating them the first time. An answer lives a day unless DefaultTTL says. A name that holds anything else is ErrInvalid, and so is Sliding: an answer lives from when it was kept.
func (*Once[V]) Run ¶
Run returns the answer kept under key, or runs fn and keeps what it returns.
While another Run of the key runs its function this one waits, until ctx ends, and then returns the answer it kept, or, when it failed, runs fn itself. fn's error is returned and keeps nothing. An answer is kept even when ctx ends after fn returned, since its work is done; an error keeping it is returned beside it.
type OpenOption ¶
type OpenOption interface {
BucketOption
CounterOption
}
func DefaultTTL ¶
func DefaultTTL(d time.Duration) OpenOption
DefaultTTL is the expiry a key gets when it is created without kv.TTL or kv.ExpireAt; a later Set or Add keeps the expiry a key has.
type Option ¶
type Option func(*callOptions)
type Options ¶
type Options struct{}
Options holds nothing a program sets yet; it is here so that an option can arrive without breaking a caller.
type Page ¶
type Page[V any] struct { Entries []Entry[V] // More says the limit, or the page's 4 MiB of values, ended the page before // the branch did. More bool // Next is the query that reads on. Next Query }
Page is one page of a Scan, from one snapshot.
type Query ¶
type Query struct {
// After, when it is not empty, starts the page after that key.
After string
Limit int // keys a page returns: 100 when zero, at most 1000
}
Query asks Scan for a page of a branch's own keys, in the byte order of their text.
type Quota ¶
type Quota struct {
// contains filtered or unexported fields
}
Quota lets each key use up to a limit in every one of its windows, such as 100 messages every five hours and 300 a week, counted by one Allow.
A window starts at a key's first use after the last one ended and lasts its span, as a counter with DefaultTTL does, so that each key resets on its own:
Window("session", 100, 5*time.Hour): first use 13:42 → resets at 18:42;
a use at 19:07 starts the next window, which resets at 00:07
An Allow checks every window and counts in each, or in none, in one durable write: two quotas asked one after the other would leave the first counted when the second refused.
func OpenQuota ¶
func OpenQuota(ctx context.Context, state *Store, name string, windows ...QuotaOption) (*Quota, error)
OpenQuota opens the quota name of kv.db, creating it the first time. A name that holds anything else is ErrInvalid. Its windows are the caller's and may change between runs: a window kept under a name no longer given is forgotten, and a limit changed counts what its window used before.
func (*Quota) AllowN ¶
AllowN uses n of key's every window at once, or none when one has no room for n. More than a window's limit never passes, and is ErrInvalid.
func (*Quota) Get ¶
Get is key's windows without using them: OK says whether one more use would pass now.
type QuotaUsage ¶
type QuotaUsage struct {
Allowance
Windows map[string]WindowUsage
}
QuotaUsage is what a quota answers a key: whether its use passed, how many more would pass now and, when it did not, how long until it would, as a limiter's Allowance says; and each window by its name.
type Raw ¶
Raw is a value as its row keeps it: nothing, an integer or bytes.
A bucket of Raw reads what a bucket of any type wrote, and what it writes is read by a bucket of the type that kept it the same way. So a program without the types, such as the server for its clients, reads and writes every bucket:
Raw{} nothing, as a struct{} of a set
Raw{Kind: RawInt, Int: 42} an integer, as a bool or an int64
Raw{Kind: RawBytes, Bytes: []byte("x")} bytes, as a string, a []byte, a float's bits or JSON
type RawConfig ¶
type RawConfig struct {
// contains filtered or unexported fields
}
RawConfig is what a config keeps, its changed fields as JSON by path, for a program that types them itself, as the server does for its clients. It checks that a value is JSON and the config within its bounds, and nothing a type would.
func OpenRawConfig ¶
OpenRawConfig opens the config name of kv.db, creating it the first time. A name that holds values, counters or a limiter is ErrInvalid.
func (*RawConfig) Change ¶
func (r *RawConfig) Change(ctx context.Context, set map[string]json.RawMessage, reset []string) error
Change keeps set and forgets reset in one transaction; every watcher of the config sees the change once it commits.
type Source ¶
type Source struct {
Path string
// Value is its JSON, or *** for a secret.
Value string
// From is "default", "env NAME", "kept", or "" for T's zero value.
From string
// Ignored says why a kept value is left out, when it is.
Ignored string
}
Source is where one field's value came from.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func Open ¶
Open opens kv.db inside the store. The store closes it and, unless it is Manual, deletes expired keys every minute, and every ten seconds while the last pass stopped at its bound with expired or cleared rows left.
func (*Store) Close ¶
Close lets the work in flight finish, writes what LoseAtMost counters hold and the renewals reads asked for, and closes kv.db; cancellation stops waiting, not the cleanup. The store calls it: an application closes the store instead.
func (*Store) Maintain ¶
func (s *Store) Maintain(ctx context.Context) (Maintenance, error)
Maintain writes what LoseAtMost counters hold and the renewals Sliding reads asked for, deletes the rows a marked Clear hid, then deletes expired keys and the values they spilled. It deletes expired keys 10,000 a transaction and at most ten transactions a call.
The store calls it every minute unless it is Manual, and every ten seconds while the last call stopped at one of those bounds with rows left. It flushes counters and renewals on their own intervals.
No read returns an expired or cleared key, whether Maintain has deleted it or not.
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx is one transaction of kv.db, given to the function passed to Store.Tx or Store.View and used by the goroutine that runs it. A bucket works in it through WithTx.
type Version ¶
type Version struct {
// contains filtered or unexported fields
}
Version is the revision of kv.db that wrote a value. It never repeats in a file, not after a delete, an expiry or a reopen, so an old IfVersion cannot pass against a key deleted and written again. Two versions are equal or not; a version marshals to text, so that it can travel to a page and back.
func (Version) MarshalText ¶
func (*Version) UnmarshalText ¶
type WindowUsage ¶
type WindowUsage struct {
Used, Limit, Left int64
// ResetAt is when the window ends, after which the next use starts
// another; zero for a window not started.
ResetAt time.Time
}
WindowUsage is one window of a key.