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

v0 source pure

Package store is the auto-id ordered collection every r/moul realm was building by hand: an avl.Tree, an int counter,...

Readme View source

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.

Overview

Package store is 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.

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

Example
1padID  width 6   for len(s) < 6 { s = "0" + s }            asciiart
2pad    width 12  for len(s) < 12 { s = "0" + s }           tictactoe
3key    width 12  the same loop, a different name           blog, crowdfund,
4                                                           englishauction,
5                                                           erc721, splitter
6idKey  width 12  the same loop, a third name               connect4
7idKey  width 12  strings.Repeat, guarded by len(s) >= 12   governor, todos
8idKey  width 12  strings.Repeat, UNGUARDED                 timecapsule
9seqKey width 16  strings.Repeat, guarded                   guestbook

Every one of them is a silent ceiling. Past the width, the padding stops and the tree starts ordering "1000000000000" before "999999999999", so the list a realm renders is simply wrong from that entry on. The unguarded variant is worse: strings.Repeat panics on a negative count, so the realm stops accepting writes rather than merely mis-sorting them. asciiart's width of 6 puts that ceiling at one million entries.

The ceiling exists because the key is a decimal string, and decimal strings do not sort numerically. This package removes the ceiling by removing the decision: keys are the big-endian bytes of a gno.land/p/nt/seqid/v0 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.

Usage

Example
 1var games store.Store             // the zero value is an empty store
 2
 3func NewGame(cur realm) int64 {
 4	id := games.Add(&Game{...})
 5	return int64(id)
 6}
 7
 8func Move(cur realm, gameID int64, cell int) {
 9	g := games.MustGet(store.ID(gameID)).(*Game)
10	...
11}
12
13func Render(path string) string {
14	for _, e := range games.PageReverse(1, 20) {   // newest first
15		g := e.Value.(*Game)
16		... e.ID ...
17	}
18}

Identifiers

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

IDs start at 1. The zero ID is never assigned, so it is usable as "absent", and Store.Get on it always misses.

Removing an entry does not free its ID. IDs are a history, not a dense index: reusing one would silently repoint an old link at a new object.

Functions 3

func ParseID

1func ParseID(s string) (ID, bool)
source

ParseID reads an ID from its decimal form, the shape it has in a Render path or a function argument.

It returns false for anything that is not a plain unsigned decimal, which includes "", "-1", "1.0", a value past uint64, and the zero ID (never assigned, so accepting it would only produce a lookup that cannot hit). Leading zeros are accepted: "007" is the ID 7.

func Named

1func Named(label string) *Store
source

Named is New with a noun for what the store holds, used in the panic Store.MustGet raises: store.Named("game") makes it "game #7 not found" instead of "store: no entry #7".

It exists so porting a realm to this package does not have to trade its own error wording for one shared implementation. The realms being replaced here panic "game not found", "proposal not found", "capsule not found"; the label keeps that and adds the id they all omitted.

func New

1func New() *Store
source

New returns an empty store.

Types 3

type Entry

struct
1type Entry struct {
2	ID    ID
3	Value any
4}
source

Entry is one ID/value pair, as returned by the paging methods.

type ID

ident
1type ID uint64
source

ID identifies one entry. It is a plain integer to the outside world and an ordered fixed-width key inside the tree.

Methods on ID

func Key

method on ID
1func (id ID) Key() string
source

Key is the avl key for the ID: the 8 big-endian bytes of the underlying seqid, whose byte order is its numeric order.

Exposed because a realm that keeps a secondary index keyed by the same ID needs the identical key, and because it is the one place the encoding is decided.

func String

method on ID
1func (id ID) String() string
source

String renders the ID in decimal, which is how realms show it and how it arrives back in a Render path.

type Store

struct
1type Store struct {
2	tree  avl.Tree
3	last  seqid.ID
4	label string
5}
source

Store is an ordered collection of values under auto-assigned IDs.

The zero Store is an empty store and is ready to use, so a realm can declare one as a package-level var without an initializer. New exists for the pointer form.

Methods on Store

func Add

method on Store
1func (s *Store) Add(v any) ID
source

Add stores v under the next ID and returns it.

It panics if the ID space is exhausted, which is seqid.ID.Next's behaviour and requires 2^64 insertions to reach.

func Each

method on Store
1func (s *Store) Each(fn func(id ID, v any))
source

Each visits every entry in ascending ID order.

It takes no stop signal on purpose: this is the common case, and a callback whose bool means "stop" reads identically to one whose bool means "continue". When iteration has to end early, say so in the name and use Store.EachUntil.

func EachReverse

method on Store
1func (s *Store) EachReverse(fn func(id ID, v any))
source

EachReverse is Store.Each in descending ID order, which is what a newest-first list wants.

func EachUntil

method on Store
1func (s *Store) EachUntil(fn func(id ID, v any) bool) bool
source

EachUntil visits entries in ascending ID order and stops when fn returns true. It reports whether it stopped early.

True means stop, matching avl.IterCbFn exactly, so a callback moved between this package and a raw tree keeps its meaning.

func Get

method on Store
1func (s *Store) Get(id ID) (any, bool)
source

Get returns the value at id, and whether it was there.

Unlike avl.Tree.Get, a nil value stored at a live ID is distinguishable from an absent one, because the second result comes from Has rather than from the value being nil.

func Has

method on Store
1func (s *Store) Has(id ID) bool
source

Has reports whether id is present.

func LastID

method on Store
1func (s *Store) LastID() ID
source

LastID is the highest ID ever assigned, or 0 when nothing has been added. It is the count of insertions, not of live entries.

func Len

method on Store
1func (s *Store) Len() int
source

Len reports how many entries are present. Removals lower it; Store.LastID does not move.

func MustGet

method on Store
1func (s *Store) MustGet(id ID) any
source

MustGet returns the value at id, or panics.

This is the "get or panic" the realms wrote eighteen times, with one implementation instead of eighteen. The panic names the id, which none of them did: "store: no entry #7", or "game #7 not found" for a store built with Named.

func Page

method on Store
1func (s *Store) Page(page, size int) []Entry
source

Page returns page number page of size entries, in ascending ID order.

Pages are 1-based, because they are shown to a reader ("page 1 of 4") and an off-by-one between the URL and the label is the bug this saves. A page past the end, or a page or size below 1, returns an empty slice rather than panicking: the page number usually comes from a Render path, which is user input.

It walks only the requested window, not the whole tree.

func PageReverse

method on Store
1func (s *Store) PageReverse(page, size int) []Entry
source

PageReverse is Store.Page in descending ID order: page 1 holds the newest entries.

func Pages

method on Store
1func (s *Store) Pages(size int) int
source

Pages reports how many pages of the given size the store holds, which is what a pager footer needs. A size below 1 gives 0.

func Remove

method on Store
1func (s *Store) Remove(id ID) (any, bool)
source

Remove deletes the entry at id and returns the value it held.

The ID is not recycled: see the package doc.

func Set

method on Store
1func (s *Store) Set(id ID, v any) (replaced bool)
source

Set writes v at an existing or explicit ID, and reports whether it replaced a value.

Use it to update an entry in place. It does not move the ID counter, so setting an ID above Store.LastID leaves a gap that Store.Add will later walk into and overwrite. Prefer Store.Add to create.

Imports 3

Source Files 4