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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

store.gno

10.17 Kb · 302 lines
  1// Package store is the auto-id ordered collection every r/moul realm was
  2// building by hand: an avl.Tree, an int counter, and a private function that
  3// zero-pads the counter into a key so the tree iterates in the order a human
  4// expects.
  5//
  6// Sixteen realm files in this repo carry that third piece. They do not agree:
  7//
  8//	padID  width 6   for len(s) < 6 { s = "0" + s }            asciiart
  9//	pad    width 12  for len(s) < 12 { s = "0" + s }           tictactoe
 10//	key    width 12  the same loop, a different name           blog, crowdfund,
 11//	                                                           englishauction,
 12//	                                                           erc721, splitter
 13//	idKey  width 12  the same loop, a third name               connect4
 14//	idKey  width 12  strings.Repeat, guarded by len(s) >= 12   governor, todos
 15//	idKey  width 12  strings.Repeat, UNGUARDED                 timecapsule
 16//	seqKey width 16  strings.Repeat, guarded                   guestbook
 17//
 18// Every one of them is a silent ceiling. Past the width, the padding stops and
 19// the tree starts ordering "1000000000000" before "999999999999", so the list
 20// a realm renders is simply wrong from that entry on. The unguarded variant is
 21// worse: strings.Repeat panics on a negative count, so the realm stops
 22// accepting writes rather than merely mis-sorting them. asciiart's width of 6
 23// puts that ceiling at one million entries.
 24//
 25// The ceiling exists because the key is a decimal string, and decimal strings
 26// do not sort numerically. This package removes the ceiling by removing the
 27// decision: keys are the big-endian bytes of a [gno.land/p/nt/seqid/v0] ID, a
 28// fixed 8 bytes whose lexicographic order IS numeric order, for every value a
 29// uint64 can hold. There is no width to pick and no width to outgrow.
 30//
 31// # Usage
 32//
 33//	var games store.Store             // the zero value is an empty store
 34//
 35//	func NewGame(cur realm) int64 {
 36//		id := games.Add(&Game{...})
 37//		return int64(id)
 38//	}
 39//
 40//	func Move(cur realm, gameID int64, cell int) {
 41//		g := games.MustGet(store.ID(gameID)).(*Game)
 42//		...
 43//	}
 44//
 45//	func Render(path string) string {
 46//		for _, e := range games.PageReverse(1, 20) {   // newest first
 47//			g := e.Value.(*Game)
 48//			... e.ID ...
 49//		}
 50//	}
 51//
 52// # Identifiers
 53//
 54// An ID is a uint64 that renders as a plain decimal number, so a realm ported
 55// to this package keeps showing "#7" exactly as it did before. Only the avl
 56// key changes, and the key was never user-visible.
 57//
 58// IDs start at 1. The zero ID is never assigned, so it is usable as "absent",
 59// and [Store.Get] on it always misses.
 60//
 61// Removing an entry does not free its ID. IDs are a history, not a dense
 62// index: reusing one would silently repoint an old link at a new object.
 63package store
 64
 65import (
 66	"strconv"
 67
 68	"gno.land/p/nt/avl/v0"
 69	"gno.land/p/nt/seqid/v0"
 70)
 71
 72// ID identifies one entry. It is a plain integer to the outside world and an
 73// ordered fixed-width key inside the tree.
 74type ID uint64
 75
 76// String renders the ID in decimal, which is how realms show it and how it
 77// arrives back in a Render path.
 78func (id ID) String() string { return strconv.FormatUint(uint64(id), 10) }
 79
 80// Key is the avl key for the ID: the 8 big-endian bytes of the underlying
 81// seqid, whose byte order is its numeric order.
 82//
 83// Exposed because a realm that keeps a secondary index keyed by the same ID
 84// needs the identical key, and because it is the one place the encoding is
 85// decided.
 86func (id ID) Key() string { return seqid.ID(id).Binary() }
 87
 88// ParseID reads an ID from its decimal form, the shape it has in a Render
 89// path or a function argument.
 90//
 91// It returns false for anything that is not a plain unsigned decimal, which
 92// includes "", "-1", "1.0", a value past uint64, and the zero ID (never
 93// assigned, so accepting it would only produce a lookup that cannot hit).
 94// Leading zeros are accepted: "007" is the ID 7.
 95func ParseID(s string) (ID, bool) {
 96	n, err := strconv.ParseUint(s, 10, 64)
 97	if err != nil || n == 0 {
 98		return 0, false
 99	}
100	return ID(n), true
101}
102
103// Entry is one ID/value pair, as returned by the paging methods.
104type Entry struct {
105	ID    ID
106	Value any
107}
108
109// Store is an ordered collection of values under auto-assigned IDs.
110//
111// The zero Store is an empty store and is ready to use, so a realm can declare
112// one as a package-level var without an initializer. [New] exists for the
113// pointer form.
114type Store struct {
115	tree  avl.Tree
116	last  seqid.ID
117	label string
118}
119
120// New returns an empty store.
121func New() *Store { return &Store{} }
122
123// Named is [New] with a noun for what the store holds, used in the panic
124// [Store.MustGet] raises: store.Named("game") makes it "game #7 not found"
125// instead of "store: no entry #7".
126//
127// It exists so porting a realm to this package does not have to trade its own
128// error wording for one shared implementation. The realms being replaced here
129// panic "game not found", "proposal not found", "capsule not found"; the label
130// keeps that and adds the id they all omitted.
131func Named(label string) *Store { return &Store{label: label} }
132
133// Add stores v under the next ID and returns it.
134//
135// It panics if the ID space is exhausted, which is [seqid.ID.Next]'s behaviour
136// and requires 2^64 insertions to reach.
137func (s *Store) Add(v any) ID {
138	id := ID(s.last.Next())
139	s.tree.Set(id.Key(), v)
140	return id
141}
142
143// Set writes v at an existing or explicit ID, and reports whether it replaced
144// a value.
145//
146// Use it to update an entry in place. It does not move the ID counter, so
147// setting an ID above [Store.LastID] leaves a gap that [Store.Add] will later
148// walk into and overwrite. Prefer [Store.Add] to create.
149func (s *Store) Set(id ID, v any) (replaced bool) {
150	return s.tree.Set(id.Key(), v)
151}
152
153// Get returns the value at id, and whether it was there.
154//
155// Unlike avl.Tree.Get, a nil value stored at a live ID is distinguishable from
156// an absent one, because the second result comes from Has rather than from the
157// value being nil.
158func (s *Store) Get(id ID) (any, bool) {
159	k := id.Key()
160	if !s.tree.Has(k) {
161		return nil, false
162	}
163	return s.tree.Get(k), true
164}
165
166// MustGet returns the value at id, or panics.
167//
168// This is the "get or panic" the realms wrote eighteen times, with one
169// implementation instead of eighteen. The panic names the id, which none of
170// them did: "store: no entry #7", or "game #7 not found" for a store built
171// with [Named].
172func (s *Store) MustGet(id ID) any {
173	v, ok := s.Get(id)
174	if !ok {
175		panic(s.missing(id))
176	}
177	return v
178}
179
180func (s *Store) missing(id ID) string {
181	if s.label == "" {
182		return "store: no entry #" + id.String()
183	}
184	return s.label + " #" + id.String() + " not found"
185}
186
187// Has reports whether id is present.
188func (s *Store) Has(id ID) bool { return s.tree.Has(id.Key()) }
189
190// Remove deletes the entry at id and returns the value it held.
191//
192// The ID is not recycled: see the package doc.
193func (s *Store) Remove(id ID) (any, bool) { return s.tree.Remove(id.Key()) }
194
195// Len reports how many entries are present. Removals lower it; [Store.LastID]
196// does not move.
197func (s *Store) Len() int { return s.tree.Size() }
198
199// LastID is the highest ID ever assigned, or 0 when nothing has been added.
200// It is the count of insertions, not of live entries.
201func (s *Store) LastID() ID { return ID(s.last) }
202
203// Each visits every entry in ascending ID order.
204//
205// It takes no stop signal on purpose: this is the common case, and a callback
206// whose bool means "stop" reads identically to one whose bool means
207// "continue". When iteration has to end early, say so in the name and use
208// [Store.EachUntil].
209func (s *Store) Each(fn func(id ID, v any)) {
210	s.tree.Iterate("", "", func(k string, v any) bool {
211		fn(keyID(k), v)
212		return false
213	})
214}
215
216// EachReverse is [Store.Each] in descending ID order, which is what a
217// newest-first list wants.
218func (s *Store) EachReverse(fn func(id ID, v any)) {
219	s.tree.ReverseIterate("", "", func(k string, v any) bool {
220		fn(keyID(k), v)
221		return false
222	})
223}
224
225// EachUntil visits entries in ascending ID order and stops when fn returns
226// true. It reports whether it stopped early.
227//
228// True means stop, matching avl.IterCbFn exactly, so a callback moved between
229// this package and a raw tree keeps its meaning.
230func (s *Store) EachUntil(fn func(id ID, v any) bool) bool {
231	return s.tree.Iterate("", "", func(k string, v any) bool {
232		return fn(keyID(k), v)
233	})
234}
235
236// EachReverseUntil is [Store.EachUntil] in descending ID order.
237func (s *Store) EachReverseUntil(fn func(id ID, v any) bool) bool {
238	return s.tree.ReverseIterate("", "", func(k string, v any) bool {
239		return fn(keyID(k), v)
240	})
241}
242
243// Page returns page number page of size entries, in ascending ID order.
244//
245// Pages are 1-based, because they are shown to a reader ("page 1 of 4") and an
246// off-by-one between the URL and the label is the bug this saves. A page past
247// the end, or a page or size below 1, returns an empty slice rather than
248// panicking: the page number usually comes from a Render path, which is user
249// input.
250//
251// It walks only the requested window, not the whole tree.
252func (s *Store) Page(page, size int) []Entry {
253	return s.slice(page, size, false)
254}
255
256// PageReverse is [Store.Page] in descending ID order: page 1 holds the newest
257// entries.
258func (s *Store) PageReverse(page, size int) []Entry {
259	return s.slice(page, size, true)
260}
261
262func (s *Store) slice(page, size int, reverse bool) []Entry {
263	if page < 1 || size < 1 {
264		return nil
265	}
266	offset := (page - 1) * size
267	out := make([]Entry, 0, size)
268	cb := func(k string, v any) bool {
269		out = append(out, Entry{ID: keyID(k), Value: v})
270		return false
271	}
272	if reverse {
273		s.tree.ReverseIterateByOffset(offset, size, cb)
274	} else {
275		s.tree.IterateByOffset(offset, size, cb)
276	}
277	return out
278}
279
280// Pages reports how many pages of the given size the store holds, which is
281// what a pager footer needs. A size below 1 gives 0.
282func (s *Store) Pages(size int) int {
283	if size < 1 {
284		return 0
285	}
286	n := s.Len()
287	return (n + size - 1) / size
288}
289
290// keyID decodes an avl key back to its ID. The keys in this tree are always
291// written by ID.Key, so a short or malformed key means the tree was written by
292// something other than this package.
293func keyID(k string) ID {
294	if len(k) != 8 {
295		panic("store: foreign key in tree")
296	}
297	var n uint64
298	for i := 0; i < 8; i++ {
299		n = n<<8 | uint64(k[i])
300	}
301	return ID(n)
302}