package web import ( "encoding/base64" "math/rand/v2" "slices" "strings" "sync" "tug/internal/transport/web/qrsvg" ) const ( BatchCapFloor = 5 batchCapSamples = 9 batchCapSeed = 0x7467 batchLabelBytes = 128 batchGroupNameBytes = 32 batchGroupsMore = 999999 batchLeftOut = 999 batchWorstExpires = "2006-01-02 15:04" ) type batchCapEstimate struct { Fixed int Row int Code int Cap int } func (e batchCapEstimate) weight(n int) int { return e.Fixed + n*(e.Row+e.Code) } func (e batchCapEstimate) capUnder(budget int) int { return max(BatchCapFloor, (budget-1-e.Fixed)/(e.Row+e.Code)) } var batchCodeSizes = struct { sync.Mutex byURLLength map[int]int }{byURLLength: map[int]int{}} func batchSampleToken(rng *rand.Rand) string { raw := make([]byte, 32) for i := range raw { raw[i] = byte(rng.UintN(256)) } return base64.RawURLEncoding.EncodeToString(raw) } func batchInviteURL(domain, token string) string { return "https://" + domain + InvitePath + token } func worstBatchView(domain, site string, n int) oneTimeBatchView { name := invitationSite(site, domain) b := OneTimeBatch{Expires: Ident(batchWorstExpires)} for range n { link := batchInviteURL(domain, strings.Repeat("A", 43)) b.Rows = append(b.Rows, OneTimeBatchRow{Label: strings.Repeat("&", batchLabelBytes), URL: Ident(link), Message: invitationMessage(name, batchWorstExpires, link)}) } for range oneTimeBadgesShown { b.Groups = append(b.Groups, Ident(strings.Repeat("g", batchGroupNameBytes))) } v := newOneTimeBatchView(b) v.GroupsMore = batchGroupsMore return v } func (k *Kit) batchWeight(v oneTimeBatchView) (int, error) { p := k.adminPage(v) body, err := k.execute("onetime-batch", p) return len(body) + k.linkedBytes(p), err } func (k *Kit) batchCodeSize(urlLength int) (int, error) { batchCodeSizes.Lock() size, ok := batchCodeSizes.byURLLength[urlLength] batchCodeSizes.Unlock() if ok { return size, nil } domain := strings.Repeat("d", urlLength-len(batchInviteURL("", strings.Repeat("A", 43)))) rng := rand.New(rand.NewPCG(batchCapSeed, uint64(urlLength))) sizes := make([]int, 0, batchCapSamples) for range batchCapSamples { link := batchInviteURL(domain, batchSampleToken(rng)) v := newOneTimeBatchView(OneTimeBatch{Rows: []OneTimeBatchRow{{URL: Ident(link)}}}) without, err := k.batchWeight(v.withCodes(nil, 0)) if err != nil { return 0, err } code, err := qrsvg.Encode(link) if err != nil { return 0, err } with, err := k.batchWeight(v.withCodes([]*qrsvg.Symbol{&code}, 1)) if err != nil { return 0, err } sizes = append(sizes, with-without) } slices.Sort(sizes) size = sizes[len(sizes)/2] batchCodeSizes.Lock() batchCodeSizes.byURLLength[urlLength] = size batchCodeSizes.Unlock() return size, nil } func (k *Kit) measureBatchCap(domain, site string) (batchCapEstimate, error) { fixed := func(n int) oneTimeBatchView { v := worstBatchView(domain, site, n).withCodes(nil, 0) v.LeftOut = batchLeftOut return v } one, err := k.batchWeight(fixed(1)) if err != nil { return batchCapEstimate{}, err } two, err := k.batchWeight(fixed(2)) if err != nil { return batchCapEstimate{}, err } code, err := k.batchCodeSize(len(batchInviteURL(domain, strings.Repeat("A", 43)))) if err != nil { return batchCapEstimate{}, err } e := batchCapEstimate{Row: two - one, Code: code} e.Fixed = one - e.Row e.Cap = e.capUnder(pageBudget) return e, nil } func (k *Kit) InvitationBatchCap() int { return k.batchCap }