Raw
  1package web
  2
  3import (
  4	"encoding/base64"
  5	"math/rand/v2"
  6	"slices"
  7	"strings"
  8	"sync"
  9
 10	"tug/internal/transport/web/qrsvg"
 11)
 12
 13const (
 14	BatchCapFloor       = 5
 15	batchCapSamples     = 9
 16	batchCapSeed        = 0x7467
 17	batchLabelBytes     = 128
 18	batchGroupNameBytes = 32
 19	batchGroupsMore     = 999999
 20	batchLeftOut        = 999
 21	batchWorstExpires   = "2006-01-02 15:04"
 22)
 23
 24type batchCapEstimate struct {
 25	Fixed int
 26	Row   int
 27	Code  int
 28	Cap   int
 29}
 30
 31func (e batchCapEstimate) weight(n int) int {
 32	return e.Fixed + n*(e.Row+e.Code)
 33}
 34
 35func (e batchCapEstimate) capUnder(budget int) int {
 36	return max(BatchCapFloor, (budget-1-e.Fixed)/(e.Row+e.Code))
 37}
 38
 39var batchCodeSizes = struct {
 40	sync.Mutex
 41	byURLLength map[int]int
 42}{byURLLength: map[int]int{}}
 43
 44func batchSampleToken(rng *rand.Rand) string {
 45	raw := make([]byte, 32)
 46	for i := range raw {
 47		raw[i] = byte(rng.UintN(256))
 48	}
 49	return base64.RawURLEncoding.EncodeToString(raw)
 50}
 51
 52func batchInviteURL(domain, token string) string {
 53	return "https://" + domain + InvitePath + token
 54}
 55
 56func worstBatchView(domain, site string, n int) oneTimeBatchView {
 57	name := invitationSite(site, domain)
 58	b := OneTimeBatch{Expires: Ident(batchWorstExpires)}
 59	for range n {
 60		link := batchInviteURL(domain, strings.Repeat("A", 43))
 61		b.Rows = append(b.Rows, OneTimeBatchRow{Label: strings.Repeat("&", batchLabelBytes), URL: Ident(link), Message: invitationMessage(name, batchWorstExpires, link)})
 62	}
 63	for range oneTimeBadgesShown {
 64		b.Groups = append(b.Groups, Ident(strings.Repeat("g", batchGroupNameBytes)))
 65	}
 66	v := newOneTimeBatchView(b)
 67	v.GroupsMore = batchGroupsMore
 68	return v
 69}
 70
 71func (k *Kit) batchWeight(v oneTimeBatchView) (int, error) {
 72	p := k.adminPage(v)
 73	body, err := k.execute("onetime-batch", p)
 74	return len(body) + k.linkedBytes(p), err
 75}
 76
 77func (k *Kit) batchCodeSize(urlLength int) (int, error) {
 78	batchCodeSizes.Lock()
 79	size, ok := batchCodeSizes.byURLLength[urlLength]
 80	batchCodeSizes.Unlock()
 81	if ok {
 82		return size, nil
 83	}
 84	domain := strings.Repeat("d", urlLength-len(batchInviteURL("", strings.Repeat("A", 43))))
 85	rng := rand.New(rand.NewPCG(batchCapSeed, uint64(urlLength)))
 86	sizes := make([]int, 0, batchCapSamples)
 87	for range batchCapSamples {
 88		link := batchInviteURL(domain, batchSampleToken(rng))
 89		v := newOneTimeBatchView(OneTimeBatch{Rows: []OneTimeBatchRow{{URL: Ident(link)}}})
 90		without, err := k.batchWeight(v.withCodes(nil, 0))
 91		if err != nil {
 92			return 0, err
 93		}
 94		code, err := qrsvg.Encode(link)
 95		if err != nil {
 96			return 0, err
 97		}
 98		with, err := k.batchWeight(v.withCodes([]*qrsvg.Symbol{&code}, 1))
 99		if err != nil {
100			return 0, err
101		}
102		sizes = append(sizes, with-without)
103	}
104	slices.Sort(sizes)
105	size = sizes[len(sizes)/2]
106	batchCodeSizes.Lock()
107	batchCodeSizes.byURLLength[urlLength] = size
108	batchCodeSizes.Unlock()
109	return size, nil
110}
111
112func (k *Kit) measureBatchCap(domain, site string) (batchCapEstimate, error) {
113	fixed := func(n int) oneTimeBatchView {
114		v := worstBatchView(domain, site, n).withCodes(nil, 0)
115		v.LeftOut = batchLeftOut
116		return v
117	}
118	one, err := k.batchWeight(fixed(1))
119	if err != nil {
120		return batchCapEstimate{}, err
121	}
122	two, err := k.batchWeight(fixed(2))
123	if err != nil {
124		return batchCapEstimate{}, err
125	}
126	code, err := k.batchCodeSize(len(batchInviteURL(domain, strings.Repeat("A", 43))))
127	if err != nil {
128		return batchCapEstimate{}, err
129	}
130	e := batchCapEstimate{Row: two - one, Code: code}
131	e.Fixed = one - e.Row
132	e.Cap = e.capUnder(pageBudget)
133	return e, nil
134}
135
136func (k *Kit) InvitationBatchCap() int {
137	return k.batchCap
138}