diff --git a/go/views/process_reader.go b/go/views/process_reader.go index edac57bb..c342bc84 100644 --- a/go/views/process_reader.go +++ b/go/views/process_reader.go @@ -104,6 +104,13 @@ func (r *ProcessReader) Follow(owner, state, tool string) (*Picture, error) { return p, nil } +// FollowBranch is Follow for a state kept per branch: the view of one branch is named `..` (the +// branch escaped as a key's name is, BranchState) and its picture `_.`, so a process follows +// several branches of one state, each its own view (design 47 G §479). Call it before Start. +func (r *ProcessReader) FollowBranch(owner, state, branch, tool string) (*Picture, error) { + return r.Follow(owner, BranchState(state, branch), tool) +} + // Adopt takes a follower a seat's client made (its Ask a seat's verb, such as the catalog half's view) as one of the // process's first views, so the client's callers write no glue: the reader sets its conditions (the unanswered first // view and view-waits, in the reader's findings), its log and clock, and its picture (`_`) hands each diff --git a/go/views/reader.go b/go/views/reader.go index bf4982c4..47bda3b5 100644 --- a/go/views/reader.go +++ b/go/views/reader.go @@ -12,6 +12,7 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/keys" ) // Reader is a module's pictures of the owners' states it used to read (novox/hq ADR 0615 §1): it @@ -86,6 +87,17 @@ func (r *Reader) Follow(owner, state, tool string) *Follower { return f } +// BranchState is the name of a per-branch state's view: `.`, the branch escaped as a key's name is (design +// 47 G §479), so a reader names the view `..`. A state that is not per branch keeps its name. +func BranchState(state, branch string) string { return state + "." + keys.EscapeName(branch) } + +// FollowBranch is Follow for a state kept per branch: the view of one branch is named `..` +// (BranchState), so a reader follows several branches of one state, each its own view and its own row of +// `_first_views` (design 47 G §479). Call it before Start. +func (r *Reader) FollowBranch(owner, state, branch, tool string) *Follower { + return r.Follow(owner, BranchState(state, branch), tool) +} + // Adopt takes a follower a seat's client made (its Ask a seat's verb, such as the catalog half's view) as one of the // reader's first views, so the client's callers write no glue: the reader sets its conditions (the unanswered first view // and view-waits, in the reader's findings), its log and clock, and hands each change to Watch, Changed and the diff --git a/go/views/views_test.go b/go/views/views_test.go index 5850ffcf..3327f1ec 100644 --- a/go/views/views_test.go +++ b/go/views/views_test.go @@ -595,6 +595,36 @@ func TestViews(t *testing.T) { // A seat's client makes the follower (its Ask a seat's verb) and both readers adopt it, so its callers write no glue: // the reader's findings (unanswered, view-waits), Keys, Get, Watch, statuses and tools, and the seat's event handed to // the client's handler. + // Design 47 G §479: a per-branch state's view is named .., several branches in one reader. + t.Run("a per-branch state is followed once per branch, each view its own row named with its escaped branch", func(t *testing.T) { + if got := BranchState("catalog-revisions", "feat/x y"); got != "catalog-revisions.feat/x=20y" { + t.Errorf("BranchState = %q", got) + } + r := &Reader{Module: "reader", Now: now} + a, b := r.FollowBranch("s", "catalog-revisions", "main", "s.read"), r.FollowBranch("s", "catalog-revisions", "feat/x", "s.read") + r.Follow("s", "plain", "s.read") + if a.State != "catalog-revisions.main" || b.State != "catalog-revisions.feat/x" { + t.Errorf("states %q %q", a.State, b.State) + } + for _, n := range []string{"s.catalog-revisions.main", "s.catalog-revisions.feat/x", "s.plain"} { + if !r.Follows(n) { + t.Errorf("%s is not followed", n) + } + } + if r.Follows("s.catalog-revisions") { + t.Errorf("the unnamed per-branch state is followed") + } + if st := r.Statuses(); len(st) != 3 || st[0].State != "catalog-revisions.main" || st[1].State != "catalog-revisions.feat/x" || st[2].State != "plain" { + t.Errorf("rows = %+v", st) + } + pr := &ProcessReader{Module: "reader"} + p1, err1 := pr.FollowBranch("s", "catalog-revisions", "main", "s.read") + p2, err2 := pr.FollowBranch("s", "catalog-revisions", "feat/x", "s.read") + if err1 != nil || err2 != nil || p1 == p2 || p1.Bucket() != "s_catalog-revisions.main" || pr.Picture("s_catalog-revisions.feat/x") != p2 { + t.Errorf("pictures %v %v %v", p1, p2, err1) + } + }) + t.Run("a follower a seat's client made is adopted: the reader's findings, reads and tools hold it, and its seat event goes to the client", func(t *testing.T) { ended := make(chan bool, 1) kept := waitBound