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

db.gno

12.74 Kb · 433 lines
  1package catalog
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
  7	"gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
  8	"gno.land/r/nym-alexiscolin000/gnoradio/data"
  9)
 10
 11// The catalog's collections in the data realm (docs/ARCHITECTURE-v1.md,
 12// section 3). P(n) is store.Pad. Records are store.Rec values; id lists are
 13// store's packed lists.
 14const (
 15	cArtists    = "catalog/artists"    // P((id-1)/chunkSize) -> artist heads (chunk)
 16	cAlists     = "catalog/alists"     // P(artist) -> Rec(track ids, album ids)
 17	cOwners     = "catalog/owners"     // owner address -> artist id
 18	cNames      = "catalog/names"      // name skeleton -> artist id
 19	cTracks     = "catalog/tracks"     // P((id-1)/chunkSize) -> track records (chunk)
 20	cGenres     = "catalog/genres"     // P(genre)/P(id>>9) -> packed track ids
 21	cAlbums     = "catalog/albums"     // P((id-1)/chunkSize) -> album records (chunk)
 22	cPlaylists  = "catalog/playlists"  // P(id) -> playlist record
 23	cUsers      = "catalog/users"      // address -> Rec(follows, tipped, playlists, reports)
 24	cLikes      = "catalog/likes"      // address/P(id>>9) -> packed liked track ids
 25	cTipped     = "catalog/tipped"     // address/P(id>>9) -> packed tipped track ids
 26	cReports    = "catalog/reports"    // P(id) -> report record (open reports only)
 27	cRemoved    = "catalog/removed"    // kind:id -> moderator's reason, "" when its artist hid it
 28	cHosts      = "catalog/hosts"      // allowed https host -> "1"
 29	cMeta       = "catalog/meta"       // "n" -> counts
 30	cTops       = "catalog/tops"       // liked|artists|fans|support -> ranks
 31	cActivity   = "catalog/activity"   // P(0..63) -> activity record (ring, head in meta)
 32	cConfig     = "catalog/config"     // admin, treasury, goal, bot, gen
 33	cSupporters = "catalog/supporters" // address -> lifetime support
 34	cMonthly    = "catalog/monthly"    // YYYY-MM -> support that month
 35	cPromo      = "catalog/promo"      // P(artist) -> promo percent
 36	cBudgets    = "catalog/budgets"    // P(artist) -> sponsored-pick settings and stats
 37	cWalletDay  = "catalog/walletday"  // address -> day|artist ids sponsored that day
 38	cClaims     = "catalog/claims"     // P(artist) -> pending claim
 39	cProofs     = "catalog/proofs"     // P(artist) -> proof URL of the last finalized claim
 40)
 41
 42var collections = []string{cArtists, cAlists, cOwners, cNames, cTracks, cGenres, cAlbums, cPlaylists, cUsers,
 43	cLikes, cTipped, cReports, cRemoved, cHosts, cMeta, cTops, cActivity, cConfig, cSupporters, cMonthly,
 44	cPromo, cBudgets, cWalletDay, cClaims, cProofs}
 45
 46const (
 47	chunkSize    = 8    // records per key of the chunked collections; tickets/nft reads artists with it: frozen
 48	maxRec       = 1500 // longest artist, track or album record when written by its author
 49	idRange      = 9    // likes, tips and genres are kept per range of 1<<idRange track ids
 50	activitySize = 64
 51)
 52
 53// setup makes the catalog's collections and its first settings, once: a
 54// later release finds them made (it is not the writer in its init).
 55func setup(cur realm, holder address) {
 56	for _, c := range collections {
 57		data.Make(cross(cur), c)
 58	}
 59	if data.Has(cMeta, "n") {
 60		return
 61	}
 62	ops := store.Ops{}.Set(cMeta, "n", newCounts()).Set(cConfig, "admin", holder.String()).
 63		Set(cHosts, "archive.org", "1").Set(cHosts, "upload.wikimedia.org", "1")
 64	data.Batch(cross(cur), ops)
 65}
 66
 67// tx reads each key from the data realm once and writes every key it
 68// changed once, in one call (data.Set or data.Batch). Every entrypoint loads
 69// what it needs, changes it in its tx, then saves; readers use one too, so a
 70// chunk is decoded once per call. A tx is never stored.
 71type tx struct {
 72	es  []entry
 73	idx map[string]int // entry key -> position in es
 74}
 75
 76type entry struct {
 77	c, k  string // collection and key
 78	val   string
 79	ok    bool // the key exists
 80	dirty bool
 81}
 82
 83func rd() *tx { return &tx{idx: map[string]int{}} }
 84
 85// at finds a key's entry, reading it from data the first time unless blind
 86// (a key about to be overwritten).
 87func (t *tx) at(c, k string, blind bool) *entry {
 88	key := c + " " + k
 89	if i, ok := t.idx[key]; ok {
 90		return &t.es[i]
 91	}
 92	e := entry{c: c, k: k}
 93	if !blind {
 94		e.val, e.ok = data.Get(c, k)
 95	}
 96	t.idx[key] = len(t.es)
 97	t.es = append(t.es, e)
 98	return &t.es[len(t.es)-1]
 99}
100
101func (t *tx) get(c, k string) (string, bool) {
102	e := t.at(c, k, false)
103	return e.val, e.ok
104}
105
106func (t *tx) val(c, k string) string { return t.at(c, k, false).val }
107
108func (t *tx) set(c, k, v string) {
109	e := t.at(c, k, false)
110	e.val, e.ok, e.dirty = v, true, true
111}
112
113// put writes a key without reading it first.
114func (t *tx) put(c, k, v string) {
115	e := t.at(c, k, true)
116	e.val, e.ok, e.dirty = v, true, true
117}
118
119func (t *tx) del(c, k string) {
120	if e := t.at(c, k, false); e.ok {
121		e.val, e.ok, e.dirty = "", false, true
122	}
123}
124
125// save writes what t changed: one data.Set or data.Remove, or Batches of
126// 64 ops (a bulk ResolveReports may change more keys than one Batch takes;
127// the transaction keeps them atomic, as in radio).
128func save(cur realm, t *tx) {
129	var ops store.Ops
130	for _, e := range t.es {
131		if !e.dirty {
132			continue
133		}
134		if e.ok {
135			ops = ops.Set(e.c, e.k, e.val)
136		} else {
137			ops = ops.Del(e.c, e.k)
138		}
139	}
140	for len(ops) > 4*64 {
141		data.Batch(cross(cur), ops[:4*64])
142		ops = ops[4*64:]
143	}
144	switch {
145	case len(ops) == 0:
146	case len(ops) > 4:
147		data.Batch(cross(cur), ops)
148	case ops[0] == store.OpSet:
149		data.Set(cross(cur), ops[1], ops[2], ops[3])
150	default:
151		data.Remove(cross(cur), ops[1], ops[2])
152	}
153}
154
155// ---- chunked records: artists, tracks and albums, chunkSize per key ----
156
157// rec returns record id of a chunked collection ("" when absent).
158func (t *tx) rec(c string, id int) string {
159	if id < 1 {
160		return ""
161	}
162	k, i := store.Slot(id, chunkSize)
163	ch := t.val(c, k)
164	if ch == "" {
165		return ""
166	}
167	return store.Field(ch, i)
168}
169
170// point is tx.rec without a tx, for the getters that read one or two
171// records: no tx to build for a call the radio makes in loops.
172func point(c string, id int) string {
173	if id < 1 {
174		return ""
175	}
176	k, i := store.Slot(id, chunkSize)
177	if ch, _ := data.Get(c, k); ch != "" {
178		return store.Field(ch, i)
179	}
180	return ""
181}
182
183// artistAt is field f of an artist's record, "" when unknown (point).
184func artistAt(id, f int) string {
185	if rec := point(cArtists, id); rec != "" {
186		return store.Field(rec, f)
187	}
188	return ""
189}
190
191// setRec replaces record id.
192func (t *tx) setRec(c string, id int, rec string) {
193	k, i := store.Slot(id, chunkSize)
194	t.set(c, k, store.With(t.val(c, k), i, rec))
195}
196
197// addRec appends record id, the collection's next one.
198func (t *tx) addRec(c string, id int, rec string) {
199	k, i := store.Slot(id, chunkSize)
200	if i == 0 {
201		t.put(c, k, store.Rec(rec))
202		return
203	}
204	t.set(c, k, store.Append(t.val(c, k), rec))
205}
206
207// fits refuses a record too long for its chunk: chunkSize records, each
208// with room left for its counters and early badges, stay under data's
209// 16 KiB value limit, so one author can never fill a chunk others share.
210func fits(rec string) string {
211	if len(rec) > maxRec {
212		panic("catalog: these details are too long, shorten the links or texts")
213	}
214	return rec
215}
216
217// ---- counts: catalog/meta "n", one field each ----
218
219// Count fields. A count is read and changed in place (store.Field and
220// store.With): converting numbers costs gas, so nothing decodes the record.
221// Reaching field i walks the i before it: what a publish changes comes first.
222const (
223	mTracks = 0
224	mGenre  = 0 // + genre id (1..NumGenres): tracks filed under it
225)
226
227const (
228	mArtists = NumGenres + 1 + iota
229	mAlbums
230	mPlaylists
231	mReports // filed
232	mOpen    // reports not yet resolved
233	mSupporters
234	mSupport // ugnot
235	mFields
236)
237
238func newCounts() string {
239	fs := make([]string, mFields)
240	for i := range fs {
241		fs[i] = "0"
242	}
243	return store.Rec(fs...)
244}
245
246func (t *tx) count(i int) int64 {
247	if m := t.val(cMeta, "n"); m != "" {
248		return text.Atoi64(store.Field(m, i))
249	}
250	return 0
251}
252
253// bump adds d to count i and returns the new value; save writes it.
254func (t *tx) bump(i int, d int64) int64 {
255	n := t.count(i) + d
256	t.set(cMeta, "n", store.With(t.val(cMeta, "n"), i, text.Itoa64(n)))
257	return n
258}
259
260// ---- id ranges: a sorted packed list per 512 ids ----
261
262func rangeKey(prefix string, id int) string { return prefix + "/" + store.Pad(id>>idRange) }
263
264// addID and removeID change a ranged id set; false when nothing changed.
265func (t *tx) addID(c, prefix string, id int) bool {
266	k := rangeKey(prefix, id)
267	list := t.val(c, k)
268	if n := store.Count(list); n == 0 || store.At(list, n-1) < id {
269		t.set(c, k, list+store.Enc(id)) // a new highest id, as every new track's
270		return true
271	}
272	list, ok := store.Add(list, id)
273	if ok {
274		t.set(c, k, list)
275	}
276	return ok
277}
278
279func (t *tx) removeID(c, prefix string, id int) bool {
280	k := rangeKey(prefix, id)
281	list, ok := store.Remove(t.val(c, k), id)
282	switch {
283	case !ok:
284	case list == "":
285		t.del(c, k)
286	default:
287		t.set(c, k, list)
288	}
289	return ok
290}
291
292// rangePage reads up to limit ids of a ranged set from offset, highest
293// first, and the set's size, one data.Page per 100 ranges.
294func rangePage(c, prefix string, offset, limit int) (ids []int, total int) {
295	end := prefix + "0"
296	// ponytail: at most 10 pages of 100 ranges (512,000 track ids); keep a
297	// counter per set if the catalog ever gets near that.
298	for p := 0; p < 10; p++ {
299		keys, lists := store.Rows(data.Page(c, prefix+"/", end, 100, true))
300		for _, l := range lists {
301			n := store.Count(l)
302			total += n
303			if offset >= n {
304				offset -= n
305				continue
306			}
307			if len(ids) < limit {
308				ids = append(ids, store.Page(l, offset, limit-len(ids), true)...)
309			}
310			offset = 0
311		}
312		if len(keys) < 100 {
313			break
314		}
315		end = keys[len(keys)-1]
316	}
317	return ids, total
318}
319
320// ---- activity ring and leaderboards ----
321
322// Activity is one entry of the public activity feed.
323type Activity struct {
324	Kind   string // publish | like | follow | tip | support | playlist | album | claim
325	By     address
326	Track  int // track, playlist or album id depending on Kind
327	Artist int
328	Amount int64
329	At     int64
330}
331
332// activity writes the next slot of the fixed-size ring (storage never
333// grows), numbers given as decimal strings. The ring's head is a key of its
334// own in catalog/meta: rewriting the counts would cost a like more.
335func (t *tx) activity(kind string, by address, track, artist, amount string) {
336	h := text.Atoi(t.val(cMeta, "head"))
337	t.put(cActivity, store.Pad(h), store.Rec(kind, by.String(), track, artist, amount, text.Itoa64(now())))
338	t.set(cMeta, "head", text.Itoa((h+1)%activitySize))
339}
340
341// activityOnce is activity for likes and follows, skipped when the newest
342// entry is already this wallet's of the same kind: like and unlike cycles,
343// or a run of likes, take one slot of the feed, not the whole ring.
344func (t *tx) activityOnce(kind string, by address, track, artist string) {
345	h := text.Atoi(t.val(cMeta, "head"))
346	last := t.val(cActivity, store.Pad((h+activitySize-1)%activitySize))
347	if store.Field(last, 0) == kind && store.Field(last, 1) == by.String() {
348		return
349	}
350	t.activity(kind, by, track, artist, "0")
351}
352
353type Rank struct {
354	Key   string
355	Value int64
356}
357
358// Leaderboard names in cTops.
359const (
360	topLiked   = "liked"   // track ids by likes
361	topArtists = "artists" // artist ids by tips
362	topFans    = "fans"    // addresses by tipped
363	topSupport = "support" // addresses by support
364)
365
366func ranks(rec string) []Rank {
367	if rec == "" {
368		return nil
369	}
370	f := store.Fields(rec)
371	out := make([]Rank, 0, len(f)/2)
372	for i := 0; i+1 < len(f); i += 2 {
373		out = append(out, Rank{f[i], text.Atoi64(f[i+1])})
374	}
375	return out
376}
377
378func (t *tx) top(name string) []Rank { return ranks(t.val(cTops, name)) }
379
380// rank moves k to v in a leaderboard, written only when the ranking
381// changed. Most changes leave it alone: when v grew (up) and is no higher
382// than the tenth value, k was not ranked either, so nothing is decoded. A
383// value that went down is looked for in the list (a substring match only
384// means "maybe ranked").
385func (t *tx) rank(name, k string, v int64, up bool) {
386	old := t.val(cTops, name)
387	if old != "" {
388		if last := store.Field(old, 2*topSize-1); last != "" && v <= text.Atoi64(last) && (up || !strings.Contains(old, k)) {
389			return
390		}
391	}
392	list := updateTop(ranks(old), k, v)
393	fs := make([]string, 0, 2*len(list))
394	for _, r := range list {
395		fs = append(fs, r.Key, text.Itoa64(r.Value))
396	}
397	if rec := store.Rec(fs...); rec != old {
398		t.set(cTops, name, rec)
399	}
400}
401
402// updateTop keeps a short leaderboard sorted by value, highest first.
403func updateTop(list []Rank, k string, v int64) []Rank {
404	if len(list) == topSize && v <= list[topSize-1].Value {
405		ranked := false
406		for _, r := range list {
407			ranked = ranked || r.Key == k
408		}
409		if !ranked {
410			return list
411		}
412	}
413	out := make([]Rank, 0, topSize+1)
414	for _, r := range list {
415		if r.Key != k {
416			out = append(out, r)
417		}
418	}
419	i := len(out)
420	for i > 0 && out[i-1].Value < v {
421		i--
422	}
423	if i >= topSize {
424		return out
425	}
426	out = append(out, Rank{})
427	copy(out[i+1:], out[i:])
428	out[i] = Rank{k, v}
429	if len(out) > topSize {
430		out = out[:topSize]
431	}
432	return out
433}