Search Apps Documentation Source Content File Folder Download Copy Actions Download

world_store.gno

8.05 Kb · 318 lines
  1package personal_world
  2
  3import (
  4	"gno.land/p/akkadia/v0/ds/btree"
  5	"gno.land/p/akkadia/v0/ds/btreeset"
  6)
  7
  8var worldStore *WorldStore = newWorldStore()
  9
 10// WorldStore owns personal world records, opaque metadata, and secondary indexes.
 11//
 12// Data shape:
 13// - worlds: worldID(uint32) -> map[string]string
 14// - metadatas: worldID(uint32) -> metadata(string)
 15// - slugIndex: slug(string) -> worldID(uint32)
 16// - nameIndex: name(string) -> worldID(uint32)
 17// - ownerIndex: owner(address string) -> *Uint32BTreeSet(worldID)
 18//
 19// Responsibilities:
 20// - allocate world IDs and store canonical world records
 21// - keep slug/name/owner indexes in sync with record mutations
 22// - store per-world admin-managed opaque metadata text
 23// - protect store-owned invariants such as duplicate names and slugs
 24// - return defensive copies from public read methods
 25//
 26// Non-responsibilities:
 27// - validate external parameter shape such as name format, slug format, CSV payloads, or pagination
 28// - interpret or semantically validate metadata; metadata is off-chain decoded admin text
 29// - perform auth, payment, freeze, migration, or event handling
 30// - parse user-facing encoded input; public exported functions must pass canonical values
 31type WorldStore struct {
 32	nextWorldID uint32
 33	worlds      *btree.Uint32BTree
 34	metadatas   *btree.Uint32BTree
 35	slugIndex   *btree.StringBTree
 36	nameIndex   *btree.StringBTree
 37	ownerIndex  *btree.StringBTree
 38}
 39
 40func newWorldStore() *WorldStore {
 41	return &WorldStore{
 42		nextWorldID: 1,
 43		worlds:      btree.NewUint32BTree(32),
 44		metadatas:   btree.NewUint32BTree(32),
 45		slugIndex:   btree.NewStringBTree(32),
 46		nameIndex:   btree.NewStringBTree(32),
 47		ownerIndex:  btree.NewStringBTree(32),
 48	}
 49}
 50
 51func (s *WorldStore) NextID() uint32 {
 52	return s.nextWorldID
 53}
 54
 55// ==================== DANGER: MUTABLE MIGRATION CAPABILITIES ====================
 56//
 57// The getters in this section expose mutable store internals.
 58// Only authorized migration exporter paths may depend on these capabilities.
 59// Runtime reads and test setup must use copy-returning methods or total helpers.
 60func (s *WorldStore) Worlds() *btree.Uint32BTree {
 61	return s.worlds
 62}
 63
 64func (s *WorldStore) Metadatas() *btree.Uint32BTree {
 65	return s.metadatas
 66}
 67
 68func (s *WorldStore) SlugIndex() *btree.StringBTree {
 69	return s.slugIndex
 70}
 71
 72func (s *WorldStore) NameIndex() *btree.StringBTree {
 73	return s.nameIndex
 74}
 75
 76func (s *WorldStore) OwnerIndex() *btree.StringBTree {
 77	return s.ownerIndex
 78}
 79
 80// ================= END DANGER: MUTABLE MIGRATION CAPABILITIES ==================
 81
 82func (s *WorldStore) Create(world map[string]string) uint32 {
 83	name := world["name"]
 84	slug := world["slug"]
 85	if s.nameIndex.Has(name) {
 86		panic("name already exists")
 87	}
 88	if s.slugIndex.Has(slug) {
 89		panic("slug already exists")
 90	}
 91
 92	id := s.nextID()
 93	stored := copyStringMap(world)
 94	stored["id"] = formatWorldID(id)
 95	s.worlds.Set(id, stored)
 96	s.slugIndex.Set(slug, id)
 97	s.nameIndex.Set(name, id)
 98	s.addOwnerIndex(id, address(stored["owner"]))
 99	return id
100}
101
102func (s *WorldStore) Update(worldID uint32, updates map[string]string) map[string]string {
103	current := s.mustGetInternal(worldID)
104	oldOwner := current["owner"]
105	newOwner := oldOwner
106	if owner, found := updates["owner"]; found {
107		newOwner = owner
108	}
109	oldName := current["name"]
110	newName := oldName
111	if name, found := updates["name"]; found {
112		newName = name
113	}
114	oldSlug := current["slug"]
115	newSlug := oldSlug
116	if slug, found := updates["slug"]; found {
117		newSlug = slug
118	}
119	if oldName != newName && s.nameIndex.Has(newName) {
120		panic("name already exists")
121	}
122	if oldSlug != newSlug && s.slugIndex.Has(newSlug) {
123		panic("slug already exists")
124	}
125
126	updated := copyStringMap(current)
127	for key, value := range updates {
128		updated[key] = value
129	}
130	updated["id"] = formatWorldID(worldID)
131
132	if oldOwner != newOwner {
133		s.removeOwnerIndex(worldID, address(oldOwner))
134		s.addOwnerIndex(worldID, address(newOwner))
135	}
136
137	if oldName != newName {
138		s.nameIndex.Remove(oldName)
139		s.nameIndex.Set(newName, worldID)
140	}
141
142	if oldSlug != newSlug {
143		s.slugIndex.Remove(oldSlug)
144		s.slugIndex.Set(newSlug, worldID)
145	}
146
147	s.worlds.Set(worldID, updated)
148	return copyStringMap(updated)
149}
150
151func (s *WorldStore) Delete(worldID uint32) {
152	world := s.mustGetInternal(worldID)
153	s.worlds.Remove(worldID)
154	s.slugIndex.Remove(world["slug"])
155	s.nameIndex.Remove(world["name"])
156	s.removeOwnerIndex(worldID, address(world["owner"]))
157	s.metadatas.Remove(worldID)
158}
159
160func (s *WorldStore) SetMetadata(worldID uint32, metadata string) {
161	s.mustGetInternal(worldID)
162	s.metadatas.Set(worldID, metadata)
163}
164
165func (s *WorldStore) GetMetadata(worldID uint32) string {
166	s.mustGetInternal(worldID)
167	metadata, found := s.metadatas.Get(worldID)
168	if !found {
169		return ""
170	}
171	return metadata.(string)
172}
173
174func (s *WorldStore) ListMetadataByIDs(worldIDs ...uint32) []string {
175	result := []string{}
176	for _, worldID := range worldIDs {
177		if !s.worlds.Has(worldID) {
178			continue
179		}
180		metadata, found := s.metadatas.Get(worldID)
181		if !found {
182			result = append(result, "")
183			continue
184		}
185		result = append(result, metadata.(string))
186	}
187	return result
188}
189
190func (s *WorldStore) MustGet(worldID uint32) map[string]string {
191	return copyStringMap(s.mustGetInternal(worldID))
192}
193
194func (s *WorldStore) Get(worldID uint32) (map[string]string, bool) {
195	world, found := s.worlds.Get(worldID)
196	if !found {
197		return nil, false
198	}
199	return copyStringMap(world.(map[string]string)), true
200}
201
202func (s *WorldStore) AssertWorldExists(worldID uint32) {
203	if !s.worlds.Has(worldID) {
204		panic("world not found: " + formatWorldID(worldID))
205	}
206}
207
208func (s *WorldStore) mustGetInternal(worldID uint32) map[string]string {
209	properties, found := s.worlds.Get(worldID)
210	if !found {
211		panic("world not found: " + formatWorldID(worldID))
212	}
213	return properties.(map[string]string)
214}
215
216func (s *WorldStore) MustGetIDBySlug(slug string) uint32 {
217	id, found := s.slugIndex.Get(slug)
218	if !found {
219		panic("world not found by slug: " + slug)
220	}
221	return id.(uint32)
222}
223
224func (s *WorldStore) MustGetIDByName(name string) uint32 {
225	id, found := s.nameIndex.Get(name)
226	if !found {
227		panic("world not found by name: " + name)
228	}
229	return id.(uint32)
230}
231
232func (s *WorldStore) Total() int {
233	return s.worlds.Size()
234}
235
236func (s *WorldStore) IsNameAvailable(name string) bool {
237	return !s.nameIndex.Has(name)
238}
239
240func (s *WorldStore) IsSlugAvailable(slug string) bool {
241	return !s.slugIndex.Has(slug)
242}
243
244func (s *WorldStore) ListIDs(page int, count int) []uint32 {
245	offset := (page - 1) * count
246	result := []uint32{}
247	s.worlds.IterateByOffset(offset, count, func(worldID uint32, _ any) bool {
248		result = append(result, worldID)
249		return false
250	})
251	return result
252}
253
254func (s *WorldStore) ListByIDs(worldIDs ...uint32) []map[string]string {
255	result := []map[string]string{}
256	for _, worldID := range worldIDs {
257		world, found := s.Get(worldID)
258		if !found {
259			continue
260		}
261		result = append(result, world)
262	}
263	return result
264}
265
266func (s *WorldStore) OwnerCount(owner address) int {
267	val, found := s.ownerIndex.Get(owner.String())
268	if !found {
269		return 0
270	}
271	return val.(*btreeset.Uint32BTreeSet).Size()
272}
273
274func (s *WorldStore) ListIDsByOwner(owner address, page int, count int) []uint32 {
275	val, found := s.ownerIndex.Get(owner.String())
276	if !found {
277		return []uint32{}
278	}
279	offset := (page - 1) * count
280	result := []uint32{}
281	val.(*btreeset.Uint32BTreeSet).IterateByOffset(offset, count, func(id uint32) bool {
282		result = append(result, id)
283		return false
284	})
285	return result
286}
287
288func (s *WorldStore) IsOwner(worldID uint32, user address) bool {
289	world := s.mustGetInternal(worldID)
290	return address(world["owner"]) == user
291}
292
293func (s *WorldStore) nextID() uint32 {
294	id := s.nextWorldID
295	s.nextWorldID++
296	return id
297}
298
299func (s *WorldStore) addOwnerIndex(worldID uint32, owner address) {
300	ownerKey := owner.String()
301	var tree *btreeset.Uint32BTreeSet
302	val, found := s.ownerIndex.Get(ownerKey)
303	if !found {
304		tree = btreeset.NewUint32BTreeSet(16)
305		s.ownerIndex.Set(ownerKey, tree)
306	} else {
307		tree = val.(*btreeset.Uint32BTreeSet)
308	}
309	tree.Set(worldID)
310}
311
312func (s *WorldStore) removeOwnerIndex(worldID uint32, owner address) {
313	val, found := s.ownerIndex.Get(owner.String())
314	if !found {
315		return
316	}
317	val.(*btreeset.Uint32BTreeSet).Remove(worldID)
318}