previous
Package previous stores a bounded event list per entity and returns a value
from the most recent matching previous event. It is useful for features such as
"previous amount for this user" or "previous amount from a different merchant".
The package uses only Operate for reads and writes, and Delete for cleanup.
A *client.Client satisfies those small interfaces.
Strengths
- Keeps previous-event feature logic in a reusable SDK package with small client
interfaces, so callers can test against narrow mocks and avoid private server
dependencies.
- Stores a bounded list per derived entity key.
MaxCount limits retained
entries, and the default of 100 prevents unbounded list growth when callers
do not choose a value.
- Uses deterministic key hashing for storage keys. The key is derived from
Name, Ref, the reference value, and FilterBy values, with FilterBy
order normalized before hashing.
- Supports both "read before write" and "write before read" flows through
IncludeCurrent, while still skipping entries with the current EventID.
- Preserves the retrieved Go value through
src.Value(req.Retrieve), which lets
callers store numeric, string, or other MessagePack-supported values without a
string-only conversion step.
Weaknesses / Tradeoffs
- Reads scan the stored list from newest to oldest in the client after one
Operate read. Very large MaxCount values increase decode and scan work, so
keep the bound close to the feature's real lookback need.
- Writes may require a second
Operate call when trimming is needed after an
append. That keeps storage bounded, but trimming adds latency on overflow
writes.
- The storage key uses SHA-1 for deterministic, server-compatible key
derivation only. Do not treat it as a security boundary or collision-resistant
identifier for untrusted protocols.
queries.MapSource returns empty strings for missing or non-string Ref,
Exclude, and FilterBy values. Missing fields can therefore group records
together unless the caller validates input first.
ListFlagAddUnique|ListFlagNoFail makes repeated identical entries
idempotent, but uniqueness is based on the full list entry. Different event
IDs with the same retrieve value are still separate entries.
Execute
Execute reads and writes in one helper call:
- With
IncludeCurrent: false, it reads the previous value first and then
writes the current event.
- With
IncludeCurrent: true, it writes the current event first and then reads;
reads still skip entries with the current EventID.
TTL, Ref, and Retrieve are required. MaxCount defaults to 100 when it
is zero or negative.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/FrogoAI/fdb-client/pkg/client"
"github.com/FrogoAI/fdb-client/pkg/queries"
"github.com/FrogoAI/fdb-client/pkg/queries/previous"
)
func main() {
c, err := client.New("localhost:3000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
src := queries.NewMapSource(map[string]any{
"standard.user_id": "user-42",
"standard.merchant_id": "merchant-7",
"amount": 89.95,
"event_id": "evt-1234",
})
result, err := previous.Execute(context.Background(), c, previous.Request{
Name: "prev_amount_diff_merchant",
Namespace: "scoring",
Ref: "standard.user_id",
Retrieve: "amount",
Exclude: "standard.merchant_id",
EventID: src.String("event_id"),
MaxCount: 50,
TTL: 24 * time.Hour,
IncludeCurrent: false,
}, src)
if err != nil {
log.Fatal(err)
}
if result.Found {
fmt.Printf("previous amount: %v\n", result.Value)
}
}
Ref identifies the entity by reading src.String(req.Ref). Retrieve is read
with src.Value, so the returned Result.Value keeps the stored Go value type.
When Exclude is set, the read returns the newest non-current entry whose
exclude value differs from the current event's exclude value.
Read Only
Use Read when another path writes events and the caller only needs the stored
previous value.
result, err := previous.Read(ctx, c, previous.Request{
Name: "prev_amount",
Namespace: "scoring",
Ref: "standard.user_id",
Retrieve: "amount",
EventID: "evt-1234",
TTL: 24 * time.Hour,
}, src)
if err != nil {
return err
}
if result.Found {
fmt.Println(result.Value)
}
Storage and Cleanup
Records are stored in set previous. The deterministic record key is derived
from Name, Ref, src.String(Ref), and any FilterBy values. FilterBy
order does not affect the key. Each list entry has this shape:
[eventID, excludeValue, retrieveValue]
Use Delete to remove the list for the entity described by the request and
source.
err := previous.Delete(ctx, c, previous.Request{
Name: "prev_amount",
Namespace: "scoring",
Ref: "standard.user_id",
Retrieve: "amount",
TTL: time.Hour,
}, src)