v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s
Build and Push Docker Image / build (push) Successful in 5m1s
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user