Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

/p/moul/kit/store/v0

Directory · 4 Files
README.md Open

gno.land/p/moul/kit/store/v0

The auto-id ordered collection every r/moul realm was building by hand: an avl.Tree, an int counter, and a private function that zero-pads the counter into a key so the tree iterates in the order a human expects.

Second package of the p/moul/kit/* layer (moul/gno-contracts#151). kit composes the existing packages, it does not replace them.

Why it exists

Sixteen realm files in this repo carry that third piece. They do not agree:

Name Width Implementation Realms
padID 6 for len(s) < 6 { s = "0" + s } asciiart
pad 12 for len(s) < 12 { s = "0" + s } tictactoe
key 12 the same loop, a different name blog, crowdfund, englishauction, erc721, splitter
idKey 12 the same loop again, a third name connect4
idKey 12 strings.Repeat, guarded by len(s) >= width governor, todos
idKey 12 strings.Repeat, unguarded timecapsule
seqKey 16 strings.Repeat, guarded guestbook

Five names, three widths, three implementations, one job, across 16 files in 12 realms.

Every one of them is a silent ceiling. The key is a decimal string, and decimal strings do not sort numerically. Below the width the zero-padding hides that; at the width the padding stops and the tree starts ordering "1000000000000" before "999999999999", so every list the realm renders is wrong from that entry on. Nothing fails, nothing logs, the order is just quietly false.

The unguarded variant fails harder. strings.Repeat panics on a negative count, so past its width timecapsule stops accepting writes rather than mis-sorting them.

asciiart's width of 6 puts its ceiling at one million entries.

Both tests live in store_test.gno (TestOrderSurvivesThePaddingCeiling, TestUnguardedPadPanicsPastItsWidth), asserting the defect rather than describing it.

The fix is to delete the decision

Keys are the big-endian bytes of a p/nt/seqid ID: a fixed 8 bytes whose lexicographic order is numeric order, for every value a uint64 can hold. There is no width to pick and no width to outgrow. seqid already solved this and was imported by exactly one file in the repo.

 1import "gno.land/p/moul/kit/store/v0"
 2
 3var games = store.Named("game")             // or `var games store.Store`
 4
 5func NewGame(cur realm) int64 {
 6    return int64(games.Add(&Game{Board: empty, X: caller()}))
 7}
 8
 9func Move(cur realm, gameID int64, cell int) {
10    g := games.MustGet(store.ID(gameID)).(*Game)   // panics "game #7 not found"
11    ...
12}
13
14func Render(path string) string {
15    for _, e := range games.PageReverse(1, 20) {   // newest first, one page
16        g := e.Value.(*Game)
17        ... e.ID ...
18    }
19}

API

New() a new empty store; the zero Store works too
Named("game") the same, with a noun for the MustGet panic
Add(v) ID store under the next ID
Set(id, v) bool write at an explicit ID, reports a replacement
Get(id) (any, bool) value and presence, so a stored nil is not "absent"
MustGet(id) any or panic store: no entry #7, or game #7 not found when named
Has(id), Remove(id), Len(), LastID()
Each(fn), EachReverse(fn) every entry, ascending / descending
EachUntil(fn) bool, EachReverseUntil(fn) bool stop when fn returns true
Page(page, size) []Entry 1-based, ascending
PageReverse(page, size) []Entry 1-based, newest first
Pages(size) int for the pager footer
ParseID(s) (ID, bool), ID.String(), ID.Key() the path round trip

IDs stay plain integers

An ID is a uint64 that renders as a decimal number, so a realm ported to this package keeps showing #7 exactly as it did. Only the avl key changes, and the key was never user-visible.

IDs start at 1, so the zero ID is usable as "absent" and ParseID("0") rejects. Removing an entry does not free its ID: IDs are a history, not a dense index, and reusing one would silently repoint an old link at a new object.

Two iteration shapes, on purpose

Each takes no stop signal. It is the common case, and a callback whose bool means "stop" reads identically to one whose bool means "continue", so the wrong guess is invisible. When iteration has to end early the name says so: EachUntil, where true means stop, matching avl.IterCbFn exactly. A callback moved between this package and a raw tree keeps its meaning.

Paging is 1-based and forgiving

Page numbers are shown to a reader ("page 1 of 4"), and an off-by-one between the URL and the label is the bug this avoids. A page past the end, or a page or size below 1, returns an empty slice rather than panicking, because the page number usually arrives from a Render path and that is user input. Only the requested window is walked, not the whole tree.

Design rule

The safe, conventional thing must be the shortest thing to type. Add is shorter than nextID++ plus a padding function, and it cannot be got wrong. That is the only mechanism that stops this helper from being written a seventeenth time.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/p/moul/kit/store/v0 dependency graph

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.