Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview/models: scoped label fold

Lewis: May this revision serve well! <lewis@tangled.org>

authored by

Lewis and committed by
Tangled
(Jul 7, 2026, 2:27 PM +0300) 151ab96e 0991aacc

+70 -1
+20 -1
appview/models/label.go
··· 1 1 package models 2 2 3 3 import ( 4 + "cmp" 4 5 "context" 5 6 "crypto/sha1" 6 7 "encoding/hex" ··· 551 552 return nil 552 553 } 553 554 555 + if op.Subject != "" && len(def.Scope) > 0 && !slices.Contains(def.Scope, op.Subject.Collection().String()) { 556 + return nil 557 + } 558 + 554 559 state.names[op.OperandKey] = def.Name 555 560 556 561 switch op.Operation { ··· 601 606 return nil 602 607 } 603 608 609 + func labelOpRank(op LabelOperation) int { 610 + if op == LabelOperationDel { 611 + return 0 612 + } 613 + return 1 614 + } 615 + 604 616 func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) { 605 617 // sort label ops in sort order first 606 618 slices.SortFunc(ops, func(a, b LabelOp) int { 607 - return a.SortAt().Compare(b.SortAt()) 619 + return cmp.Or( 620 + a.SortAt().Compare(b.SortAt()), 621 + cmp.Compare(a.Did, b.Did), 622 + cmp.Compare(a.Rkey, b.Rkey), 623 + cmp.Compare(labelOpRank(a.Operation), labelOpRank(b.Operation)), 624 + cmp.Compare(a.OperandKey, b.OperandKey), 625 + cmp.Compare(a.OperandValue, b.OperandValue), 626 + ) 608 627 }) 609 628 610 629 // apply ops in sequence
+50
appview/models/label_test.go
··· 1 + package models 2 + 3 + import ( 4 + "slices" 5 + "testing" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + ) 10 + 11 + func TestApplyLabelOps_FutureDatedOrderIndependent(t *testing.T) { 12 + def := &LabelDefinition{ 13 + Did: "did:plc:boltless", Rkey: "st", Name: "status", 14 + ValueType: ValueType{Type: ConcreteTypeString}, 15 + Scope: []string{"sh.tangled.repo.issue"}, 16 + Multiple: false, 17 + } 18 + key := def.AtUri().String() 19 + ctx := &LabelApplicationCtx{Defs: map[string]*LabelDefinition{key: def}} 20 + subject := syntax.ATURI("at://did:plc:boltless/sh.tangled.repo.issue/issue1") 21 + 22 + mk := func(rkey, val string, performed time.Time) LabelOp { 23 + return LabelOp{ 24 + Did: "did:plc:boltless", Rkey: rkey, Subject: subject, 25 + Operation: LabelOperationAdd, OperandKey: key, OperandValue: val, 26 + PerformedAt: performed, 27 + } 28 + } 29 + 30 + earlier := time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC) 31 + later := time.Date(2200, 1, 1, 0, 0, 0, 0, time.UTC) 32 + 33 + fold := func(ops []LabelOp) []string { 34 + st := NewLabelState() 35 + ctx.ApplyLabelOps(st, ops) 36 + out := st.LabelNameValues() 37 + slices.Sort(out) 38 + return out 39 + } 40 + 41 + forward := fold([]LabelOp{mk("aaa", "open", earlier), mk("bbb", "closed", later)}) 42 + reverse := fold([]LabelOp{mk("bbb", "closed", later), mk("aaa", "open", earlier)}) 43 + 44 + if !slices.Equal(forward, reverse) { 45 + t.Fatalf("fold must be order-independent for future-dated ops: forward=%v reverse=%v", forward, reverse) 46 + } 47 + if !slices.Equal(forward, []string{"status:closed"}) { 48 + t.Fatalf("the later createdAt must win regardless of ingest order, got %v", forward) 49 + } 50 + }