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}