db.gno
19.29 Kb · 637 lines
1package radio
2
3import (
4 "strings"
5
6 "gno.land/p/nym-alexiscolin000/gnoradio/blocks/v0"
7 "gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
8 "gno.land/r/nym-alexiscolin000/gnoradio/data"
9)
10
11// The radio's collections in the data realm (docs/ARCHITECTURE-v1.md,
12// section 3). P(n) is store.Pad, T(t) a 12-digit unix time. Numbers inside
13// the hot records are fixed-width base-64 digits (store.Num, store.Fixed): a decimal
14// conversion costs the VM several times more.
15const (
16 cStations = "radio/stations" // P(station) -> Rec(epoch, ring, live, rotation header, schedule, recent picks, weekly top[, ring rotation, ring index])
17 cRot = "radio/rot" // P(station)/b/P(block) -> block; P(station)/g/P(group) -> block sums (p/blocks)
18 cHomes = "radio/homes" // P((id-1)/homesChunk) -> Rec of homes entries (the genre stations of a track), one per synced track
19 cMeta = "radio/meta" // "flow" -> flow record
20 cCurators = "radio/curators" // address -> curator record (curators.gno)
21 cTops = "radio/tops" // "all" -> this week's top curators across stations (a station's is in its record)
22 cDropped = "radio/dropped" // P(station)/P(track) -> "1"
23 cNotes = "radio/notes" // P(station)/T(start) -> reporters, or the strike a hide gave (notes.gno)
24 cMuted = "radio/muted" // address -> unix time its dedications are refused until
25 cStrikes = "radio/strikes" // address -> unix time one of its dedications was last hidden
26 cSponsor = "radio/sponsor" // address/P(station) -> open sponsored pick (sponsor.gno)
27 cActivity = "radio/activity" // P(0..63) -> activity record (a ring, its head in the flow record)
28 cConfig = "radio/config" // "admin" (mirror of the admin role), "modbot" (hex key)
29)
30
31var collections = []string{cStations, cRot, cHomes, cMeta, cCurators, cTops, cDropped, cNotes,
32 cMuted, cStrikes, cSponsor, cActivity, cConfig}
33
34const (
35 homesChunk = 16 // homes entries per key
36 maxRecord = 16 << 10 // the data realm's longest value
37)
38
39// setup makes the radio's collections and its first records, once: a later
40// release finds them made (it is not the writer in its init).
41func setup(cur realm, holder address) {
42 for _, c := range collections {
43 data.Make(cross(cur), c)
44 }
45 if data.Has(cMeta, "flow") {
46 return
47 }
48 ops := store.Ops{}.Set(cMeta, "flow", strings.Repeat("0", flowLen)).Set(cConfig, "admin", holder.String())
49 data.Batch(cross(cur), ops)
50}
51
52// Fixed-width base-64 numbers are store.Num, store.Fixed and
53// store.AppendFixed. dec is store.Digits' decoding table, for the digits
54// the hot loops read in place: a call per digit costs about 1% of a pick.
55var dec = decTable()
56
57func decTable() (d [128]int64) {
58 for i := 0; i < len(store.Digits); i++ {
59 d[store.Digits[i]] = int64(i)
60 }
61 return d
62}
63
64// ---- tx: every key read once and written once ----
65
66// tx reads each key from the data realm once and writes every key it
67// changed once, in one call (data.Set or data.Batch); the stations it
68// loaded are decoded once and encoded back when changed. Every entrypoint
69// loads what it needs, changes it in its tx, then saves; readers use one
70// too. A tx is never stored.
71type tx struct {
72 es []entry
73 idx map[string]int // entry key -> position in es
74 sts [numStations]*station
75 flw string // the flow record, once read (flowRec)
76}
77
78type entry struct {
79 c, k string // collection and key
80 val string
81 ok bool // the key exists
82 dirty bool
83}
84
85func rd() *tx { return &tx{idx: map[string]int{}} }
86
87// at finds a key's entry, reading it from data the first time unless blind
88// (a key about to be overwritten).
89func (t *tx) at(c, k string, blind bool) *entry {
90 key := c + " " + k
91 if i, ok := t.idx[key]; ok {
92 return &t.es[i]
93 }
94 e := entry{c: c, k: k}
95 if !blind {
96 e.val, e.ok = data.Get(c, k)
97 }
98 t.idx[key] = len(t.es)
99 t.es = append(t.es, e)
100 return &t.es[len(t.es)-1]
101}
102
103func (t *tx) get(c, k string) (string, bool) {
104 e := t.at(c, k, false)
105 return e.val, e.ok
106}
107
108func (t *tx) val(c, k string) string { return t.at(c, k, false).val }
109
110func (t *tx) has(c, k string) bool { return t.at(c, k, false).ok }
111
112func (t *tx) set(c, k, v string) {
113 e := t.at(c, k, false)
114 e.val, e.ok, e.dirty = v, true, true
115}
116
117// put writes a key without reading it first.
118func (t *tx) put(c, k, v string) {
119 e := t.at(c, k, true)
120 e.val, e.ok, e.dirty = v, true, true
121}
122
123func (t *tx) del(c, k string) bool {
124 e := t.at(c, k, false)
125 if !e.ok {
126 return false
127 }
128 e.val, e.ok, e.dirty = "", false, true
129 return true
130}
131
132// save writes what t changed: one data.Set or data.Remove, or Batches of
133// 64 ops (a Sync or a RefreshArtist may change more keys than one Batch
134// takes; the transaction keeps them atomic).
135func save(cur realm, t *tx) {
136 for _, st := range t.sts {
137 if st != nil && st.dirty {
138 sched := st.raw
139 if st.decoded {
140 sched = encodeSchedule(st.sched)
141 }
142 f := []string{store.Fixed(st.epoch, 6), store.Fixed(int64(st.ring), 6), store.Fixed(int64(st.live), 4), st.h, sched, st.picks, st.top}
143 if st.ringed() {
144 f = append(f, st.rot, st.idx)
145 }
146 // A pick is refused once its station's record would leave less
147 // than recordHeadroom, and the writes a pick or a Sync makes on
148 // other stations wait while theirs does (full): this is a last
149 // resort.
150 rec := store.Rec(f...)
151 if len(rec) > maxRecord {
152 panic("radio: this station is full for now, try again later")
153 }
154 t.set(cStations, store.Pad(st.id), rec)
155 st.dirty = false
156 }
157 }
158 var ops store.Ops
159 for _, e := range t.es {
160 if !e.dirty {
161 continue
162 }
163 if e.ok {
164 ops = ops.Set(e.c, e.k, e.val)
165 } else {
166 ops = ops.Del(e.c, e.k)
167 }
168 }
169 for len(ops) > 4*64 {
170 data.Batch(cross(cur), ops[:4*64])
171 ops = ops[4*64:]
172 }
173 switch {
174 case len(ops) == 0:
175 case len(ops) > 4:
176 data.Batch(cross(cur), ops)
177 case ops[0] == store.OpSet:
178 data.Set(cross(cur), ops[1], ops[2], ops[3])
179 default:
180 data.Remove(cross(cur), ops[1], ops[2])
181 }
182}
183
184// ---- the flow record: radio/meta "flow" ----
185
186// The flow record is fixed-width: synced (4 digits), the number of dropped
187// slots (4), the activity ring's next slot (1), the week of the top across
188// stations and its last score when it holds topCurators (0 else) (4 and
189// 4: a pick that scores less leaves it as it is, unread), then "1" for each
190// genre station with a live track ("0" else, index = genre): Main relays
191// one of those (hourGenre). Every call reads it.
192const (
193 fSynced = 0
194 fDrops = 4
195 fHead = 8
196 fTopWeek = 9
197 fTopLast = 13
198 fLive = 17
199 flowLen = fLive + NumGenres + 1
200)
201
202// flowRec is the flow record, read once per tx: every call reads it. The data
203// realm is permanent: a record an earlier release wrote shorter is padded
204// with "0" (a later one's extra fields are kept as they are).
205func (t *tx) flowRec() string {
206 if t.flw == "" {
207 t.flw = t.val(cMeta, "flow")
208 if len(t.flw) < flowLen {
209 t.flw += strings.Repeat("0", flowLen-len(t.flw))
210 }
211 }
212 return t.flw
213}
214
215func (t *tx) flowAt(at, w int) int { return int(store.Num(t.flowRec(), at, w)) }
216
217func (t *tx) setFlowAt(at, w, v int) {
218 f := t.flowRec()
219 if n := store.Fixed(int64(v), w); n != f[at:at+w] {
220 t.flw = f[:at] + n + f[at+w:]
221 t.set(cMeta, "flow", t.flw)
222 }
223}
224
225func (t *tx) synced() int { return t.flowAt(fSynced, 4) }
226
227// hasLive reports whether a genre station has a live track.
228func (t *tx) hasLive(genre int) bool {
229 return genre >= 1 && genre <= NumGenres && t.flowRec()[fLive+genre] == '1'
230}
231
232// dropped reports whether the admin dropped a track from a station.
233func (t *tx) dropped(stationID, trackID int) bool {
234 return t.flowAt(fDrops, 4) > 0 && t.has(cDropped, dropKey(stationID, trackID))
235}
236
237func dropKey(stationID, trackID int) string { return store.Pad(stationID) + "/" + store.Pad(trackID) }
238
239// ---- stations ----
240
241// station is a station loaded in a tx: its header and schedule are one
242// record (cStations); its rotation is read and written a block at a time
243// (cRot), but for New and Choice: their rings (at most 500 slots) and the
244// index of the tracks in them are part of their record, which a pick
245// changes anyway.
246// The schedule is decoded on first use (slots): a call that only needs the
247// time paused under it (paused) or a picker (picker) reads it as it is.
248type station struct {
249 t *tx
250 id int
251 genre int // 0 = every genre
252 epoch int64
253 ring int // New and Choice: joins so far; the slot reused next is ring % keep
254 live int // rotation slots with a duration > 0
255 h string // the rotation's header (p/blocks)
256 raw string // the encoded schedule
257 picks string // the picks of the last replayGap (lastPicked)
258 top string // this week's top curators here (curators.gno)
259 rot string // New and Choice: the group then the blocks of their ring
260 idx string // and the ring's tracks: sorted entries, the track (4 digits) and its slot (2)
261 sched []Slot // sorted by Start, short (folded as it ages), once decoded
262 decoded bool
263 dirty bool
264 pz int64 // paused(pzAt), while pzOK: the schedule did not move since
265 pzAt int64
266 pzOK bool
267}
268
269// station loads a station once per tx.
270func (t *tx) station(id int) *station {
271 if id < 0 || id >= numStations {
272 panic("radio: unknown station")
273 }
274 if st := t.sts[id]; st != nil {
275 return st
276 }
277 st := &station{t: t, id: id, genre: id}
278 if id > NumGenres {
279 st.genre = 0
280 }
281 if rec := t.val(cStations, store.Pad(id)); rec != "" {
282 f := store.Fields(rec)
283 st.epoch, st.ring, st.live, st.h, st.raw, st.picks, st.top = store.Num(f[0], 0, 6), int(store.Num(f[1], 0, 6)), int(store.Num(f[2], 0, 4)), f[3], f[4], f[5], f[6]
284 if len(f) > 7 {
285 st.rot, st.idx = f[7], f[8]
286 }
287 }
288 t.sts[id] = st
289 return st
290}
291
292// size is the length of the record save writes for the station, its
293// schedule measured as encodeSchedule writes it, without writing it.
294func (st *station) size() int {
295 n := 1 + 3*7 + 6 + 6 + 4 + len(st.h) + len(st.picks) + len(st.top)
296 if st.ringed() {
297 n += 3*2 + len(st.rot) + len(st.idx)
298 }
299 if !st.decoded {
300 return n + len(st.raw)
301 }
302 var prevEnd int64
303 for i := range st.sched {
304 s := &st.sched[i]
305 switch gap := s.Start - prevEnd; {
306 case gap == 0:
307 n++
308 case gap < 0:
309 n += 7
310 default:
311 n += 2
312 for gap>>6 != 0 {
313 gap >>= 6
314 n++
315 }
316 }
317 n += 25 + len(s.By) + len(s.Note)
318 prevEnd = s.Start + s.Dur
319 }
320 return n
321}
322
323// full reports whether the record keeps less than recordHeadroom: a new pick
324// is refused, and a write a pick makes on the side (a Choice or New join)
325// waits. A side write adds far less than recordHeadroom, so
326// the record never reaches the data realm's limit (maxRecord).
327func (st *station) full() bool { return st.size() > maxRecord-recordHeadroom }
328
329// slots is the decoded schedule.
330func (st *station) slots() []Slot {
331 if !st.decoded {
332 st.sched, st.decoded = decodeSchedule(st.raw), true
333 }
334 return st.sched
335}
336
337// setSlots replaces the schedule.
338func (st *station) setSlots(s []Slot) {
339 st.sched, st.decoded, st.dirty, st.pzOK = s, true, true, false
340}
341
342// addLive counts a slot going live (d = 1) or silent (-1); the flow record
343// keeps which genre stations have one.
344func (st *station) addLive(d int) {
345 st.live += d
346 st.dirty = true
347 if st.id >= 1 && st.id <= NumGenres && (st.live > 0) != st.t.hasLive(st.id) {
348 on := 0
349 if st.live > 0 {
350 on = 1
351 }
352 st.t.setFlowAt(fLive+st.id, 1, on)
353 }
354}
355
356// ---- a station's rotation: p/blocks over its keys ----
357
358func (st *station) ringed() bool { return st.id == NewStation || st.id == ChoiceStation }
359
360// A ring's rotation (one group) is its group's sums, 4 digits per block,
361// then its blocks, blocks.Size slots each but the last.
362const blockLen = blocks.Size * 6
363
364// ringBlocks is where a ring's blocks start.
365func (st *station) ringBlocks() int { return 4 * ((st.count() + blocks.Size - 1) / blocks.Size) }
366
367// group and block read group g and block b of the rotation.
368func (st *station) group(g int) string {
369 if st.ringed() {
370 return st.rot[:st.ringBlocks()]
371 }
372 return st.t.val(cRot, store.Pad(st.id)+"/g/"+store.Pad(g))
373}
374
375func (st *station) block(b int) string {
376 if !st.ringed() {
377 return st.t.val(cRot, store.Pad(st.id)+"/b/"+store.Pad(b))
378 }
379 at := st.ringBlocks() + b*blockLen
380 if at >= len(st.rot) {
381 return ""
382 }
383 if at+blockLen < len(st.rot) {
384 return st.rot[at : at+blockLen]
385 }
386 return st.rot[at:]
387}
388
389// setRotVal writes group g and block b; st.h is still the rotation's
390// header before the change.
391func (st *station) setRotVal(g int, grp string, b int, blk string) {
392 if !st.ringed() {
393 st.t.set(cRot, store.Pad(st.id)+"/g/"+store.Pad(g), grp)
394 st.t.set(cRot, store.Pad(st.id)+"/b/"+store.Pad(b), blk)
395 return
396 }
397 bs := st.rot[st.ringBlocks():]
398 at, end := b*blockLen, b*blockLen+blockLen
399 if at > len(bs) {
400 at = len(bs)
401 }
402 if end > len(bs) {
403 end = len(bs)
404 }
405 st.rot, st.dirty = grp+bs[:at]+blk+bs[end:], true
406}
407
408func (st *station) total() int64 { return blocks.Total(st.h) }
409func (st *station) count() int { return blocks.Len(st.h) }
410
411func (st *station) rotSlot(i int) (int, int64) {
412 if i < 0 || i >= st.count() {
413 panic("radio: rotation index out of range")
414 }
415 _, b := blocks.Where(i)
416 return blocks.Slot(st.block(b), i)
417}
418
419// find returns the rotation slot playing at position pos of the loop.
420func (st *station) find(pos int64) (slot, id int, offset int64, ok bool) {
421 g, rest, ok := blocks.FindGroup(st.h, pos)
422 if !ok {
423 return 0, 0, 0, false
424 }
425 b, rest := blocks.FindBlock(st.group(g), g, rest)
426 slot, id, offset = blocks.FindSlot(st.block(b), b, rest)
427 return slot, id, offset, true
428}
429
430func (st *station) prefixBefore(i int) int64 {
431 g, b := blocks.Where(i)
432 return blocks.PrefixBefore(st.h, st.group(g), st.block(b), i)
433}
434
435// appendRot adds a slot at the end of the rotation and returns its index.
436func (st *station) appendRot(id int, dur int64) int {
437 g, b := blocks.Where(st.count())
438 h, grp, blk, i := blocks.Append(st.h, st.group(g), st.block(b), id, dur)
439 st.setRotVal(g, grp, b, blk)
440 st.h, st.dirty = h, true
441 return i
442}
443
444// setRot replaces slot i's id and duration.
445func (st *station) setRot(i, id int, dur int64) {
446 g, b := blocks.Where(i)
447 h, grp, blk := blocks.Set(st.h, st.group(g), st.block(b), i, id, dur)
448 st.setRotVal(g, grp, b, blk)
449 st.h, st.dirty = h, true
450}
451
452// ---- schedules ----
453
454// A schedule is its slots in order, each a record that starts with the gap
455// since the previous slot's end (one digit "0" when it follows it, else the
456// number of digits and the gap, "z" and the start if it overlaps), then its
457// body: "L" (listener), the duration (2 digits), the track (4), the artist
458// (4), the time booked (6), the refund (4), "1" when its note is hidden, and
459// its address and note, each after its length (1 and 2 digits). A body is reused as long as its slot keeps
460// its duration and note, so moving slots together costs no encoding.
461
462func decodeSchedule(s string) []Slot {
463 out := make([]Slot, 0, len(s)/8+1)
464 for c := (cursor{s: s}); c.next(); {
465 b := s[c.b:c.i]
466 x := Slot{Track: int(dec[b[3]]<<18 | dec[b[4]]<<12 | dec[b[5]]<<6 | dec[b[6]]), Start: c.start, Dur: c.end - c.start, body: b, bdur: c.end - c.start}
467 bl := int(dec[b[22]])
468 x.artist, x.At, x.Pay, x.NoteHidden = int(store.Num(b, 7, 4)), store.Num(b, 11, 6), store.Num(b, 17, 4), b[21] == '1'
469 x.By, x.Note = address(b[23:23+bl]), b[25+bl:]
470 out = append(out, x)
471 }
472 return out
473}
474
475func encodeSchedule(ss []Slot) string {
476 b := make([]byte, 0, 8*len(ss)+64)
477 var prevEnd int64
478 for i := range ss {
479 s := &ss[i]
480 switch {
481 case s.body == "":
482 s.body = s.encodeBody()
483 case s.bdur != s.Dur:
484 s.body = s.body[:1] + string(store.AppendFixed(make([]byte, 0, 2), s.Dur, 2)) + s.body[3:]
485 }
486 s.bdur = s.Dur
487 switch gap := s.Start - prevEnd; {
488 case gap == 0:
489 b = append(b, '0')
490 case gap < 0:
491 b = store.AppendFixed(append(b, 'z'), s.Start, 6)
492 default:
493 n := 1
494 for gap>>(6*n) != 0 {
495 n++
496 }
497 b = store.AppendFixed(append(b, store.Digits[n]), gap, n)
498 }
499 b = append(b, s.body...)
500 prevEnd = s.Start + s.Dur
501 }
502 return string(b)
503}
504
505func (s *Slot) encodeBody() string {
506 b := make([]byte, 0, 32+len(s.By)+len(s.Note))
507 b = store.AppendFixed(store.AppendFixed(store.AppendFixed(append(b, 'L'), s.Dur, 2), int64(s.Track), 4), int64(s.artist), 4)
508 b = store.AppendFixed(store.AppendFixed(b, s.At, 6), s.Pay, 4)
509 if s.NoteHidden {
510 b = append(b, '1')
511 } else {
512 b = append(b, '0')
513 }
514 b = append(store.AppendFixed(b, int64(len(s.By)), 1), s.By...)
515 return string(append(store.AppendFixed(b, int64(len(s.Note)), 2), s.Note...))
516}
517
518// cursor walks an encoded schedule a slot at a time without decoding it:
519// after next, the slot's body is s[b:i] and it plays from start to end.
520type cursor struct {
521 s string
522 i, b int
523 start, end int64
524}
525
526func (c *cursor) next() bool {
527 s, i := c.s, c.i
528 if i >= len(s) {
529 return false
530 }
531 start := c.end
532 switch ch := s[i]; ch {
533 case '0':
534 i++
535 case 'z':
536 start, i = store.Num(s, i+1, 6), i+7
537 default:
538 n := int(dec[s[i]])
539 start, i = c.end+store.Num(s, i+1, n), i+1+n
540 }
541 c.b, c.start, c.end = i, start, start+(dec[s[i+1]]<<6|dec[s[i+2]])
542 k := i + 23 + int(dec[s[i+22]])
543 c.i = k + 2 + int(dec[s[k]]<<6|dec[s[k+1]])
544 return true
545}
546
547// ---- where a track rotates: Main at id-1, New and Choice in their ring
548// index, genre stations in homes ----
549
550const idxW = 6 // a ring index entry
551
552// ringFind returns the position of a track in the ring index, or where it goes.
553func (st *station) ringFind(trackID int) (int, bool) {
554 lo, hi := 0, len(st.idx)/idxW
555 for lo < hi {
556 mid := (lo + hi) / 2
557 if int(store.Num(st.idx, idxW*mid, 4)) < trackID {
558 lo = mid + 1
559 } else {
560 hi = mid
561 }
562 }
563 return lo, lo < len(st.idx)/idxW && int(store.Num(st.idx, idxW*lo, 4)) == trackID
564}
565
566// ringSlot is a track's slot in the ring.
567func (st *station) ringSlot(trackID int) (int, bool) {
568 p, ok := st.ringFind(trackID)
569 if !ok {
570 return 0, false
571 }
572 return int(store.Num(st.idx, idxW*p+4, 2)), true
573}
574
575// ringIndex files a track at a slot of the ring, at position p of the
576// index (ringFind), replacing the entry there when found.
577func (st *station) ringIndex(p int, found bool, trackID, slot int) {
578 end := idxW * p
579 if found {
580 end += idxW
581 }
582 st.idx, st.dirty = st.idx[:idxW*p]+store.Fixed(int64(trackID), 4)+store.Fixed(int64(slot), 2)+st.idx[end:], true
583}
584
585// ringUnindex forgets a track that left the ring's slot and returns where
586// it was in the index (-1: it was not there).
587func (st *station) ringUnindex(trackID, slot int) int {
588 if p, ok := st.ringFind(trackID); ok && int(store.Num(st.idx, idxW*p+4, 2)) == slot {
589 st.idx, st.dirty = st.idx[:idxW*p]+st.idx[idxW*p+idxW:], true
590 return p
591 }
592 return -1
593}
594
595// A homes entry is 5 digits per genre station: the station (1) and the slot (4).
596
597func homesKey(trackID int) (string, int) {
598 return store.Pad((trackID - 1) / homesChunk), (trackID - 1) % homesChunk
599}
600
601// homes returns a synced track's entry.
602func (t *tx) homes(trackID int) string {
603 k, i := homesKey(trackID)
604 if ch := t.val(cHomes, k); ch != "" {
605 return store.Field(ch, i)
606 }
607 return ""
608}
609
610func (t *tx) setHomes(trackID int, e string) {
611 k, i := homesKey(trackID)
612 t.set(cHomes, k, store.With(t.val(cHomes, k), i, e))
613}
614
615// appendHomes files the entry of the next synced track.
616func (t *tx) appendHomes(trackID int, e string) {
617 k, i := homesKey(trackID)
618 if i == 0 {
619 t.put(cHomes, k, store.Rec(e))
620 return
621 }
622 t.set(cHomes, k, store.Append(t.val(cHomes, k), e))
623}
624
625func homeEntry(stationID, slot int) string {
626 return store.Fixed(int64(stationID), 1) + store.Fixed(int64(slot), 4)
627}
628
629// homeSlot finds a station's slot in an entry.
630func homeSlot(e string, stationID int) (int, bool) {
631 for i := 0; i+5 <= len(e); i += 5 {
632 if int(dec[e[i]]) == stationID {
633 return int(store.Num(e, i+1, 4)), true
634 }
635 }
636 return 0, false
637}