views: the hand-act recorder and the finding writer come from the caller as interfaces, no hand-acts or health import (ADR 0635 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
+3
-14
@@ -29,10 +29,7 @@ type Emitter struct {
|
||||
TooLarge func(state, key string, size int)
|
||||
// Findings writes and clears the owner's finding of a change not said past UnsaidAfter (UnsaidKind, its own state
|
||||
// `findings`); nil says it in the log only.
|
||||
Findings interface {
|
||||
Put(key string, value []byte) error
|
||||
Delete(key string) error
|
||||
}
|
||||
Findings FindingWriter
|
||||
// UnsaidAfter is how long a change may stay not said before it is the owner's finding; zero is Bound.
|
||||
UnsaidAfter time.Duration
|
||||
// Wait pauses before a change not said is said again (its attempt so far); it answers false when ctx ended. Nil is
|
||||
@@ -180,11 +177,7 @@ func (e Emitter) raiseUnsaid(state string, u jetstream.KeyValueEntry, first time
|
||||
if e.Findings == nil {
|
||||
return false
|
||||
}
|
||||
key, value, err := UnsaidFinding(e.Owner, state, u.Key(), u.Revision(), first, cause.Error(), e.now())
|
||||
if err == nil {
|
||||
err = e.Findings.Put(key, value)
|
||||
}
|
||||
if err != nil {
|
||||
if err := e.Findings.Put(UnsaidFinding(e.Owner, state, u.Key(), u.Revision(), first, cause.Error(), e.now())); err != nil {
|
||||
e.logf("views: the finding of %s's change not said could not be written: %v", state, err)
|
||||
return false
|
||||
}
|
||||
@@ -193,11 +186,7 @@ func (e Emitter) raiseUnsaid(state string, u jetstream.KeyValueEntry, first time
|
||||
|
||||
// clearUnsaid clears the finding once the change is said; it answers whether it is cleared.
|
||||
func (e Emitter) clearUnsaid(state string) bool {
|
||||
key, _, err := UnsaidFinding(e.Owner, state, "", 0, time.Time{}, "", e.now())
|
||||
if err == nil {
|
||||
err = e.Findings.Delete(key)
|
||||
}
|
||||
if err != nil {
|
||||
if err := e.Findings.Delete(UnsaidFinding(e.Owner, state, "", 0, time.Time{}, "", e.now())); err != nil {
|
||||
e.logf("views: the finding of %s's change not said could not be cleared: %v", state, err)
|
||||
return false
|
||||
}
|
||||
|
||||
+26
-11
@@ -4,8 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// The findings a views part writes in its own state `findings`, which the condition keeper raises and clears
|
||||
@@ -17,9 +15,30 @@ const (
|
||||
UnansweredKind = "first-view-unanswered"
|
||||
)
|
||||
|
||||
// Finding is what a views part says is wrong with a state it owns or follows, in plain fields: Kind and ID name it (a module-scoped
|
||||
// finding of the observer), Summary, Headline and Explanation say it. It carries no layout: the writer a part gives
|
||||
// (FindingWriter) encodes it as the condition keeper reads it, which is the seat mesh-conditions' contract, not this
|
||||
// package's (novox/hq ADR 0634 §4, ADR 0635 §4).
|
||||
type Finding struct {
|
||||
Observer string
|
||||
Kind string
|
||||
ID string
|
||||
Summary string
|
||||
Headline string
|
||||
Explanation string
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// FindingWriter writes and clears a part's findings in its own state `findings`, where the condition keeper raises and
|
||||
// clears conditions from them (novox/hq ADR 0564 §2): the part gives the one that encodes a Finding as that state's layout.
|
||||
type FindingWriter interface {
|
||||
Put(f Finding) error
|
||||
Delete(f Finding) error
|
||||
}
|
||||
|
||||
// UnsaidFinding is the owner's finding of a change of state it has not said on its views family since since: every
|
||||
// reader of the state stands still at the change before it until it is said, which clears this.
|
||||
func UnsaidFinding(owner, state, key string, revision uint64, since time.Time, cause string, at time.Time) (string, []byte, error) {
|
||||
func UnsaidFinding(owner, state, key string, revision uint64, since time.Time, cause string, at time.Time) Finding {
|
||||
id := owner + "." + state
|
||||
summary := fmt.Sprintf("%s has not said the change of %s.%s at %d on its views family since %s (%s): its readers stand still before it",
|
||||
owner, state, key, revision, since.UTC().Format(time.RFC3339), strings.TrimSpace(cause))
|
||||
@@ -30,7 +49,7 @@ func UnsaidFinding(owner, state, key string, revision uint64, since time.Time, c
|
||||
}
|
||||
|
||||
// TooLargeFinding is the owner's finding of state's key too large to say whole on its views family.
|
||||
func TooLargeFinding(owner, state, key string, size int, at time.Time) (string, []byte, error) {
|
||||
func TooLargeFinding(owner, state, key string, size int, at time.Time) Finding {
|
||||
id := owner + "." + state + "." + key
|
||||
summary := fmt.Sprintf("%s's %s.%s holds %d bytes, more than one views event carries (%d): every reader asks its first view again at each change of it", owner, state, key, size, MaxValueBytes)
|
||||
return finding(owner, TooLargeKind, id, summary,
|
||||
@@ -41,7 +60,7 @@ func TooLargeFinding(owner, state, key string, size int, at time.Time) (string,
|
||||
|
||||
// UnansweredFinding is a reader's finding of its first view of s unanswered past its bound (ADR 0615 §6): it names the
|
||||
// owner and the tool. A first view answered at revision 0 with nothing in it is said too, with the operator's step.
|
||||
func UnansweredFinding(reader string, s Status, at time.Time) (string, []byte, error) {
|
||||
func UnansweredFinding(reader string, s Status, at time.Time) Finding {
|
||||
id := reader + "." + s.Owner + "." + s.State
|
||||
summary := fmt.Sprintf("%s has no first view of %s's %s: %s has not answered since %s (%s)", reader, s.Owner, s.State, s.Tool,
|
||||
s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error))
|
||||
@@ -51,10 +70,6 @@ func UnansweredFinding(reader string, s Status, at time.Time) (string, []byte, e
|
||||
"Look at %s's runtime of its own: is it running and connected?", reader, s.Owner, s.State, s.Tool, s.Owner), at)
|
||||
}
|
||||
|
||||
func finding(observer, kind, id, summary, headline, explanation string, at time.Time) (string, []byte, error) {
|
||||
key := health.FindingKey(health.ScopeModule, id, kind)
|
||||
value, err := health.EncodeFinding(health.Finding{Key: key, Kind: kind, Subject: health.Subject{Scope: health.ScopeModule, ID: id},
|
||||
Severity: health.Warning, Summary: summary, Headline: headline, Explanation: explanation,
|
||||
Evidence: []health.Evidence{{At: at.UTC(), Said: summary}}, Source: observer + "." + kind, Observer: observer, At: at.UTC()})
|
||||
return key, value, err
|
||||
func finding(observer, kind, id, summary, headline, explanation string, at time.Time) Finding {
|
||||
return Finding{Observer: observer, Kind: kind, ID: id, Summary: summary, Headline: headline, Explanation: explanation, At: at.UTC()}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package views
|
||||
|
||||
import "errors"
|
||||
|
||||
// HandAct is a hand act a part records before it does it: what is done, why, and the cause and verb that name it.
|
||||
type HandAct struct {
|
||||
What, Why, Cause, Verb string
|
||||
}
|
||||
|
||||
// HandActs records a hand act (novox/hq ADR 0565 clause 11) before the part does it; a part gives the one that records
|
||||
// it through its hand-acts client. An act that is not taken returns an error that says nothing was recorded,
|
||||
// and the part then does nothing. A nil HandActs records nothing, so a re-read by hand is refused.
|
||||
type HandActs interface {
|
||||
RecordHandAct(HandAct) error
|
||||
}
|
||||
|
||||
func recordHandAct(acts HandActs, a HandAct) error {
|
||||
if acts == nil {
|
||||
return errors.New("no way to record a hand act is given")
|
||||
}
|
||||
return acts.RecordHandAct(a)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ type ProcessReader struct {
|
||||
Call func(key string, body any) (json.RawMessage, error)
|
||||
// Findings is the reader's own state `findings`, where the condition keeper raises and clears the condition of a
|
||||
// first view unanswered past its bound (ADR 0615 §6); nil says it in the log only.
|
||||
Findings func() jetstream.KeyValue
|
||||
Findings func() FindingWriter
|
||||
Logf func(string, ...any)
|
||||
// Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds.
|
||||
Now func() time.Time
|
||||
@@ -141,11 +141,12 @@ func (r *ProcessReader) finding(s Status, raised bool) {
|
||||
if r.Now != nil {
|
||||
at = r.Now()
|
||||
}
|
||||
key, value, err := UnansweredFinding(r.Module, s, at)
|
||||
if err == nil && raised {
|
||||
_, err = kv.Put(context.Background(), key, value)
|
||||
} else if err == nil {
|
||||
err = kv.Delete(context.Background(), key)
|
||||
f := UnansweredFinding(r.Module, s, at)
|
||||
var err error
|
||||
if raised {
|
||||
err = kv.Put(f)
|
||||
} else {
|
||||
err = kv.Delete(f)
|
||||
}
|
||||
if err != nil {
|
||||
r.logf("views: the finding of the first view %s is not written: %v", s.Tool, err)
|
||||
|
||||
+27
-30
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
)
|
||||
|
||||
// Reader is a module's pictures of the owners' states it used to read (novox/hq ADR 0615 §1): it
|
||||
@@ -23,16 +22,13 @@ type Reader struct {
|
||||
Module string
|
||||
// Findings writes and clears this module's finding of an unanswered first view (its own state `findings`); nil
|
||||
// says it in the log only.
|
||||
Findings interface {
|
||||
Put(key string, value []byte) error
|
||||
Delete(key string) error
|
||||
}
|
||||
Logf func(string, ...any)
|
||||
Findings FindingWriter
|
||||
Logf func(string, ...any)
|
||||
// Changed is told of each key a followed state changes, after the picture holds it (state is `<owner>.<state>`).
|
||||
Changed func(state, key string, value json.RawMessage, deleted bool)
|
||||
// Acts records a re-read by hand through the seat mesh-hand-acts before it is asked (the zero Recorder records nothing
|
||||
// Acts records a re-read by hand before it is asked (nil records nothing
|
||||
// and the re-read is refused).
|
||||
Acts handacts.Recorder
|
||||
Acts HandActs
|
||||
// Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds.
|
||||
Now func() time.Time
|
||||
Bound time.Duration
|
||||
@@ -94,10 +90,6 @@ func (r *Reader) Follow(owner, state, tool string) *Follower {
|
||||
return f
|
||||
}
|
||||
|
||||
func (r *Reader) findingKey(s Status) (string, []byte, error) {
|
||||
return UnansweredFinding(r.Module, s, r.nowTime())
|
||||
}
|
||||
|
||||
func (r *Reader) nowTime() time.Time {
|
||||
if r.Now != nil {
|
||||
return r.Now().UTC()
|
||||
@@ -110,11 +102,7 @@ func (r *Reader) raise(s Status) {
|
||||
if r.Findings == nil {
|
||||
return
|
||||
}
|
||||
key, value, err := r.findingKey(s)
|
||||
if err == nil {
|
||||
err = r.Findings.Put(key, value)
|
||||
}
|
||||
if err != nil {
|
||||
if err := r.Findings.Put(UnansweredFinding(r.Module, s, r.nowTime())); err != nil {
|
||||
r.logf("the finding of the unanswered first view %s could not be written: %v", s.Tool, err)
|
||||
}
|
||||
}
|
||||
@@ -123,11 +111,7 @@ func (r *Reader) clear(s Status) {
|
||||
if r.Findings == nil {
|
||||
return
|
||||
}
|
||||
key, _, err := r.findingKey(s)
|
||||
if err == nil {
|
||||
err = r.Findings.Delete(key)
|
||||
}
|
||||
if err != nil {
|
||||
if err := r.Findings.Delete(UnansweredFinding(r.Module, s, r.nowTime())); err != nil {
|
||||
r.logf("the finding of the first view %s could not be cleared: %v", s.Tool, err)
|
||||
}
|
||||
}
|
||||
@@ -310,12 +294,12 @@ func (r *Reader) Tools(prefix string) []stdio.Tool {
|
||||
if len(fs) == 0 {
|
||||
return nil, fmt.Errorf("%q is not a state this module follows", want)
|
||||
}
|
||||
// A hand act, recorded through the seat mesh-hand-acts before it is done; one that cannot be recorded is
|
||||
// A hand act, recorded before it is done; one that cannot be recorded is
|
||||
// said and nothing is asked.
|
||||
why := "the operator asked"
|
||||
if _, err := r.Acts.Record(handacts.Request{What: "asked a first view again by hand (" + r.Module + ")", Why: why,
|
||||
if err := recordHandAct(r.Acts, HandAct{What: "asked a first view again by hand (" + r.Module + ")", Why: why,
|
||||
Cause: "first-view-again", Verb: "first-view-again"}); err != nil {
|
||||
return nil, fmt.Errorf("the hand act was not recorded through mesh-hand-acts, so nothing was asked again: %w", err)
|
||||
return nil, fmt.Errorf("the hand act was not recorded, so nothing was asked again: %w", err)
|
||||
}
|
||||
for _, f := range fs {
|
||||
if err := f.Again(context.Background(), why); err != nil {
|
||||
@@ -328,14 +312,27 @@ func (r *Reader) Tools(prefix string) []stdio.Tool {
|
||||
}
|
||||
|
||||
// OwnFindings writes a reader's findings into its own state `findings` through its runtime, where the condition keeper
|
||||
// raises and clears conditions from them: the Findings of a Reader whose module keeps the state `findings`.
|
||||
type OwnFindings struct{}
|
||||
// raises and clears conditions from them: the Findings of a Reader whose module keeps the state `findings`. Encode is
|
||||
// the module's: it turns a Finding into the state's key and value as the seat mesh-conditions' contract lays them out.
|
||||
type OwnFindings struct {
|
||||
Encode func(Finding) (key string, value []byte, err error)
|
||||
}
|
||||
|
||||
// Put writes one finding.
|
||||
func (OwnFindings) Put(key string, value []byte) error {
|
||||
_, err := stdio.State("findings").Put(key, json.RawMessage(value))
|
||||
func (o OwnFindings) Put(f Finding) error {
|
||||
key, value, err := o.Encode(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stdio.State("findings").Put(key, json.RawMessage(value))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete clears one finding.
|
||||
func (OwnFindings) Delete(key string) error { return stdio.State("findings").Delete(key) }
|
||||
func (o OwnFindings) Delete(f Finding) error {
|
||||
key, _, err := o.Encode(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return stdio.State("findings").Delete(key)
|
||||
}
|
||||
|
||||
+5
-6
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
)
|
||||
|
||||
// StatusState is a reader's own state of its first views (design 25 § 12.2): at StatusKey each first view's Status, as
|
||||
@@ -94,9 +93,9 @@ func HearAgain(ctx context.Context, kv jetstream.KeyValue, followers []*Follower
|
||||
// StatusTools are a reader's read tool of its first views (read) and its re-read tool (again), in its tools bundle,
|
||||
// over its StatusState: read lists what the process last said of each; again writes a hand ask the process takes,
|
||||
// naming the caller the runtime proved, and is recorded in the status's by-hand count and why. again is a hand act:
|
||||
// acts records it through the seat mesh-hand-acts before the ask is written, and one the seat does not take is said and
|
||||
// nothing is asked (the zero Recorder records nothing, so again refuses).
|
||||
func StatusTools(reader, read, again string, st stdio.KeptState, acts handacts.Recorder, now func() time.Time) []stdio.Tool {
|
||||
// acts records it before the ask is written, and one that is not taken is said and nothing is asked (nil acts
|
||||
// record nothing, so again refuses).
|
||||
func StatusTools(reader, read, again string, st stdio.KeptState, acts HandActs, now func() time.Time) []stdio.Tool {
|
||||
return []stdio.Tool{
|
||||
{Name: read,
|
||||
Description: fmt.Sprintf("%s's first views of the owners' states it follows (novox/hq ADR 0615): per owner and state, "+
|
||||
@@ -139,9 +138,9 @@ func StatusTools(reader, read, again string, st stdio.KeptState, acts handacts.R
|
||||
return nil, errors.New("again names the owner, the state and why")
|
||||
}
|
||||
a := HandAsk{V: Version, Owner: owner, State: state, Why: strings.TrimSpace(why), By: stdio.CallerOf(args), At: now().UTC()}
|
||||
if _, err := acts.Record(handacts.Request{What: fmt.Sprintf("asked %s's first view of %s's %s again by hand", reader, owner, state),
|
||||
if err := recordHandAct(acts, HandAct{What: fmt.Sprintf("asked %s's first view of %s's %s again by hand", reader, owner, state),
|
||||
Why: a.Why, Cause: "first-view-again", Verb: again}); err != nil {
|
||||
return nil, fmt.Errorf("the hand act was not recorded through mesh-hand-acts, so nothing was asked again: %w", err)
|
||||
return nil, fmt.Errorf("the hand act was not recorded, so nothing was asked again: %w", err)
|
||||
}
|
||||
if _, err := st.Put(AgainKey(owner, state), a); err != nil {
|
||||
return nil, fmt.Errorf("the hand ask is not written: %w", err)
|
||||
|
||||
+25
-29
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/handacts"
|
||||
)
|
||||
|
||||
// mapSource is a state held in memory, as a Source.
|
||||
@@ -252,10 +251,10 @@ func TestViews(t *testing.T) {
|
||||
t.Run("a module's reader answers Keys and Get from its pictures, refuses a state it does not follow or has no first view of, and writes and clears its finding", func(t *testing.T) {
|
||||
acted := 0
|
||||
fs := &findingsMap{}
|
||||
r := &Reader{Module: "reader", Findings: fs, Bound: time.Nanosecond, Wait: time.Millisecond, Acts: handacts.Recorder{Ask: func(string, any) (json.RawMessage, error) {
|
||||
r := &Reader{Module: "reader", Findings: fs, Bound: time.Nanosecond, Wait: time.Millisecond, Acts: actsFunc(func(HandAct) error {
|
||||
acted++
|
||||
return json.RawMessage(`{"log":"mesh-hand-acts"}`), nil
|
||||
}}}
|
||||
return nil
|
||||
})}
|
||||
f := r.Follow("o", "modules", "o.indexer_read_modules")
|
||||
down := 2
|
||||
f.Ask = func(ctx context.Context, after string) (Answer, error) {
|
||||
@@ -354,22 +353,13 @@ func TestViews(t *testing.T) {
|
||||
}
|
||||
_ = w.Stop()
|
||||
})
|
||||
t.Run("a reader's again tool records its hand act through mesh-hand-acts before it writes the ask, and asks nothing when the seat does not take it", func(t *testing.T) {
|
||||
var recorded []handacts.Request
|
||||
t.Run("a reader's again tool records its hand act before it writes the ask, and asks nothing when the act is not taken", func(t *testing.T) {
|
||||
var recorded []HandAct
|
||||
take := errors.New("no responders")
|
||||
acts := handacts.Recorder{Ask: func(key string, body any) (json.RawMessage, error) {
|
||||
if key != "seat:mesh-hand-acts.hand-act" {
|
||||
return nil, fmt.Errorf("asked %s", key)
|
||||
}
|
||||
raw, _ := json.Marshal(body)
|
||||
var q handacts.Request
|
||||
_ = json.Unmarshal(raw, &q)
|
||||
acts := actsFunc(func(q HandAct) error {
|
||||
recorded = append(recorded, q)
|
||||
if take != nil {
|
||||
return nil, take
|
||||
}
|
||||
return json.RawMessage(`{"log":"mesh-hand-acts"}`), nil
|
||||
}}
|
||||
return take
|
||||
})
|
||||
again := StatusTools("reader", "reader_first_views", "reader_first_view_again", stdio.State(StatusState), acts, now)[1]
|
||||
args := map[string]any{"owner": "o", "state": "modules", "why": "doubted"}
|
||||
if _, err := again.Run(args); err == nil || !strings.Contains(err.Error(), "nothing was asked again") || len(recorded) != 1 {
|
||||
@@ -450,15 +440,16 @@ func TestViews(t *testing.T) {
|
||||
t.Errorf("%d failures: %d findings written, %d cleared, %d left, said %v", c.fails, fs.puts, fs.deletes, len(fs.m), said)
|
||||
}
|
||||
if c.puts > 0 {
|
||||
key, value, _ := UnsaidFinding("o", "s", "a", 1, made, "bus away", clock)
|
||||
if !strings.Contains(key, UnsaidKind) || !strings.Contains(key, "o.s") || !strings.Contains(string(value), "o has not said the change of s.a at 1") {
|
||||
t.Errorf("the finding is %s: %s", key, value)
|
||||
f := UnsaidFinding("o", "s", "a", 1, made, "bus away", clock)
|
||||
if f.Kind != UnsaidKind || f.ID != "o.s" || f.Observer != "o" || !strings.Contains(f.Summary, "o has not said the change of s.a at 1") {
|
||||
t.Errorf("the finding is %+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A finding a run before a restart raised is cleared by the first change said after it.
|
||||
key, value, _ := UnsaidFinding("o", "s", "a", 1, made, "bus away", made)
|
||||
fs := &findingsMap{m: map[string][]byte{key: value}}
|
||||
left0 := UnsaidFinding("o", "s", "a", 1, made, "bus away", made)
|
||||
key := left0.Kind + "/" + left0.ID
|
||||
fs := &findingsMap{m: map[string]Finding{key: left0}}
|
||||
said, fails = nil, 0
|
||||
em.Findings = fs
|
||||
updates = make(chan jetstream.KeyValueEntry, 1)
|
||||
@@ -484,22 +475,27 @@ func TestViews(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// findingsMap is a module's own findings state in memory.
|
||||
// findingsMap is a module's own findings state in memory, keyed `<kind>/<id>`.
|
||||
type findingsMap struct {
|
||||
m map[string][]byte
|
||||
m map[string]Finding
|
||||
puts, deletes int
|
||||
}
|
||||
|
||||
func (f *findingsMap) Put(k string, v []byte) error {
|
||||
func (f *findingsMap) Put(v Finding) error {
|
||||
if f.m == nil {
|
||||
f.m = map[string][]byte{}
|
||||
f.m = map[string]Finding{}
|
||||
}
|
||||
f.m[k] = v
|
||||
f.m[v.Kind+"/"+v.ID] = v
|
||||
f.puts++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *findingsMap) Delete(k string) error { delete(f.m, k); f.deletes++; return nil }
|
||||
func (f *findingsMap) Delete(v Finding) error { delete(f.m, v.Kind+"/"+v.ID); f.deletes++; return nil }
|
||||
|
||||
// actsFunc records a hand act through a test's function.
|
||||
type actsFunc func(HandAct) error
|
||||
|
||||
func (f actsFunc) RecordHandAct(a HandAct) error { return f(a) }
|
||||
|
||||
// kvEntry is one change of a bucket as a watch hands it.
|
||||
type kvEntry struct {
|
||||
|
||||
Reference in New Issue
Block a user