103 lines
2.8 KiB
Go
103 lines
2.8 KiB
Go
package storage
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/metazla/meta-core/internal/claim"
|
|
)
|
|
|
|
// Vote is one signer's claim on one key, as the merge sees it. Pid identifies
|
|
// the signer (one vote per signer per key per group); Uid is carried for the
|
|
// stateless /api/merge, where claims arrive without a local pid.
|
|
type Vote struct {
|
|
Pid int64
|
|
Uid string
|
|
Value claim.Value
|
|
TS uint64
|
|
}
|
|
|
|
// MergeResult is the merged value of one key.
|
|
type MergeResult struct {
|
|
Value string
|
|
Nsrc int // signers agreeing on the winning value
|
|
OK bool
|
|
}
|
|
|
|
// Merge is the one merge rule (spec §7), shared by the apply loop, the
|
|
// ?as=<uid> overlay and POST /api/merge:
|
|
//
|
|
// 1. a signer counts once per key: its newest vote wins (it may have voted on
|
|
// several cids of the group);
|
|
// 2. the viewer's own live text vote wins outright (viewer == "" for the
|
|
// shared view);
|
|
// 3. retractions and ciphertext drop out — only text merges;
|
|
// 4. support: votes grouped by normalised value, most signers wins;
|
|
// 5. tie → the value first seen (oldest ts) — stable, and rewriting does not
|
|
// win;
|
|
// 6. tie → byte-wise smallest value, so every peer agrees.
|
|
//
|
|
// The stored value is the exact text of the oldest vote of the winning group.
|
|
func Merge(votes []Vote, viewer string) MergeResult {
|
|
newest := make(map[string]Vote, len(votes))
|
|
for _, v := range votes {
|
|
id := signerID(v)
|
|
if cur, ok := newest[id]; !ok || v.TS > cur.TS {
|
|
newest[id] = v
|
|
}
|
|
}
|
|
type bucket struct {
|
|
n int
|
|
minTS uint64
|
|
exemplar string
|
|
}
|
|
buckets := make(map[string]*bucket)
|
|
for _, v := range newest {
|
|
if v.Value.Kind != claim.KindText {
|
|
continue
|
|
}
|
|
if viewer != "" && v.Uid == viewer {
|
|
return MergeResult{Value: v.Value.Text, Nsrc: 1, OK: true}
|
|
}
|
|
norm := normalizeValue(v.Value.Text)
|
|
b := buckets[norm]
|
|
if b == nil {
|
|
buckets[norm] = &bucket{n: 1, minTS: v.TS, exemplar: v.Value.Text}
|
|
continue
|
|
}
|
|
b.n++
|
|
if v.TS < b.minTS || (v.TS == b.minTS && v.Value.Text < b.exemplar) {
|
|
b.minTS, b.exemplar = v.TS, v.Value.Text
|
|
}
|
|
}
|
|
var best *bucket
|
|
for _, b := range buckets {
|
|
switch {
|
|
case best == nil,
|
|
b.n > best.n,
|
|
b.n == best.n && b.minTS < best.minTS,
|
|
b.n == best.n && b.minTS == best.minTS && b.exemplar < best.exemplar:
|
|
best = b
|
|
}
|
|
}
|
|
if best == nil {
|
|
return MergeResult{}
|
|
}
|
|
return MergeResult{Value: best.exemplar, Nsrc: best.n, OK: true}
|
|
}
|
|
|
|
func signerID(v Vote) string {
|
|
if v.Uid != "" {
|
|
return v.Uid
|
|
}
|
|
return "#" + strconv.FormatInt(v.Pid, 10)
|
|
}
|
|
|
|
// normalizeValue folds case and surrounding whitespace so "The Matrix" and
|
|
// "the matrix " count as the same value. (Unicode NFC folding is not applied:
|
|
// the module carries no normalisation table; values written by one plugin
|
|
// family are already consistent.)
|
|
func normalizeValue(s string) string {
|
|
return strings.ToLower(strings.TrimSpace(s))
|
|
}
|