world_store.gno
5.06 Kb · 192 lines
1package chunk
2
3import "gno.land/p/akkadia/v0/ds/btree"
4
5// WorldStore owns chunk world records and secondary indexes.
6//
7// Data shape:
8// - worlds: worldID(uint32) -> map[string]string
9// - slugIndex: slug(string) -> worldID(uint32)
10// - nameIndex: name(string) -> worldID(uint32)
11//
12// Responsibilities:
13// - store canonical world records by world ID
14// - keep slug/name indexes in sync with record mutations
15// - protect store-owned invariants such as duplicate world IDs, names, and slugs
16// - return defensive copies from public read methods
17//
18// Non-responsibilities:
19// - validate external parameter shape such as slug format, seed range, CSV payloads, or pagination
20// - interpret or semantically validate properties; properties are canonical map entries
21// - perform auth, freeze, migration, or event handling
22// - parse user-facing encoded input; public exported functions must pass canonical values
23type WorldStore struct {
24 worlds *btree.Uint32BTree
25 slugIndex *btree.StringBTree
26 nameIndex *btree.StringBTree
27}
28
29var worldStore = newWorldStore()
30
31func newWorldStore() *WorldStore {
32 return &WorldStore{
33 worlds: btree.NewUint32BTree(32),
34 slugIndex: btree.NewStringBTree(32),
35 nameIndex: btree.NewStringBTree(32),
36 }
37}
38
39// ==================== DANGER: MUTABLE MIGRATION CAPABILITIES ====================
40//
41// The getters in this section expose mutable store internals.
42// Only authorized migration exporter paths may depend on these capabilities.
43// Runtime reads and test setup must use copy-returning methods or total helpers.
44func (s *WorldStore) Worlds() *btree.Uint32BTree {
45 return s.worlds
46}
47
48func (s *WorldStore) SlugIndex() *btree.StringBTree {
49 return s.slugIndex
50}
51
52func (s *WorldStore) NameIndex() *btree.StringBTree {
53 return s.nameIndex
54}
55
56// ================= END DANGER: MUTABLE MIGRATION CAPABILITIES ==================
57
58func (s *WorldStore) Create(worldID uint32, world map[string]string) map[string]string {
59 name := world["name"]
60 slug := world["slug"]
61 if _, found := s.worlds.Get(worldID); found {
62 panic("world already exists: " + formatWorldID(worldID))
63 }
64 if s.nameIndex.Has(name) {
65 panic("name already exists: " + name)
66 }
67 if s.slugIndex.Has(slug) {
68 panic("slug already exists: " + slug)
69 }
70
71 stored := copyStringMap(world)
72 stored["id"] = formatWorldID(worldID)
73 s.worlds.Set(worldID, stored)
74 s.slugIndex.Set(slug, worldID)
75 s.nameIndex.Set(name, worldID)
76 return copyStringMap(stored)
77}
78
79func (s *WorldStore) Update(worldID uint32, updates map[string]string) map[string]string {
80 current := s.mustGetInternal(worldID)
81 oldName := current["name"]
82 newName := oldName
83 if name, found := updates["name"]; found {
84 newName = name
85 }
86 oldSlug := current["slug"]
87 newSlug := oldSlug
88 if slug, found := updates["slug"]; found {
89 newSlug = slug
90 }
91 renameName := newName != oldName
92 renameSlug := newSlug != oldSlug
93
94 if renameName {
95 if s.nameIndex.Has(newName) {
96 panic("name already exists: " + newName)
97 }
98 }
99 if renameSlug {
100 if s.slugIndex.Has(newSlug) {
101 panic("slug already exists: " + newSlug)
102 }
103 }
104
105 updated := copyStringMap(current)
106 for key, value := range updates {
107 updated[key] = value
108 }
109 updated["id"] = formatWorldID(worldID)
110
111 if renameName {
112 s.nameIndex.Remove(oldName)
113 s.nameIndex.Set(newName, worldID)
114 }
115 if renameSlug {
116 s.slugIndex.Remove(oldSlug)
117 s.slugIndex.Set(newSlug, worldID)
118 }
119
120 s.worlds.Set(worldID, updated)
121 return copyStringMap(updated)
122}
123
124func (s *WorldStore) MustGet(worldID uint32) map[string]string {
125 return copyStringMap(s.mustGetInternal(worldID))
126}
127
128func (s *WorldStore) AssertExists(worldID uint32) {
129 if !s.worlds.Has(worldID) {
130 panic("world not found: " + formatWorldID(worldID))
131 }
132}
133
134func (s *WorldStore) Has(worldID uint32) bool {
135 return s.worlds.Has(worldID)
136}
137
138func (s *WorldStore) IsNameAvailable(name string) bool {
139 return !s.nameIndex.Has(name)
140}
141
142func (s *WorldStore) IsSlugAvailable(slug string) bool {
143 return !s.slugIndex.Has(slug)
144}
145
146func (s *WorldStore) mustGetInternal(worldID uint32) map[string]string {
147 s.AssertExists(worldID)
148 value, _ := s.worlds.Get(worldID)
149 return value.(map[string]string)
150}
151
152func (s *WorldStore) Get(worldID uint32) (map[string]string, bool) {
153 value, found := s.worlds.Get(worldID)
154 if !found {
155 return nil, false
156 }
157 return copyStringMap(value.(map[string]string)), true
158}
159
160func (s *WorldStore) MustGetBySlug(slug string) map[string]string {
161 worldID, found := s.slugIndex.Get(slug)
162 if !found {
163 panic("world not found by slug: " + slug)
164 }
165 return s.MustGet(worldID.(uint32))
166}
167
168func (s *WorldStore) Total() int {
169 return s.worlds.Size()
170}
171
172func (s *WorldStore) List(page int, count int) []map[string]string {
173 offset := (page - 1) * count
174 result := []map[string]string{}
175 s.worlds.IterateByOffset(offset, count, func(_ uint32, value any) bool {
176 result = append(result, copyStringMap(value.(map[string]string)))
177 return false
178 })
179 return result
180}
181
182func (s *WorldStore) ListByIDs(worldIDs ...uint32) []map[string]string {
183 result := []map[string]string{}
184 for _, worldID := range worldIDs {
185 world, found := s.Get(worldID)
186 if !found {
187 continue
188 }
189 result = append(result, world)
190 }
191 return result
192}