G A-29: views.HeldWriter, the writer of a part whose findings are raised as they happen, observes the findings it holds
Seeded from the state's keys before it starts, held under a lock of its own, its Observe answers them; Write and Clear keep the held set. The watchman's copy moves here. Builds: design 47 G A-29, §686 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// HeldWriter is the finding writer of a part whose findings are raised as they happen, not judged afresh (design 47 G A-29,
|
||||
// §686): it holds the findings it has written, seeded from the keys of its state `findings` before it starts, under a lock of
|
||||
// its own (never the part's: the writer calls Observe under its own lock while the part writes under its), and its Observe
|
||||
// answers them, so a statement says exactly what the part holds. Write and Clear update the held set and then the writer's.
|
||||
// It is a Writer[F, C, S, A] with that Observe, so no part writes the adapter.
|
||||
type HeldWriter[F, C, S, A any] struct {
|
||||
*Writer[F, C, S, A]
|
||||
mu sync.Mutex
|
||||
held map[string]F
|
||||
}
|
||||
|
||||
// NewHeldWriter makes the writer for cfg, whose Observe is none (a given Observe is refused): the findings that stand in
|
||||
// cfg.State are read at once, each key's value as the writer kept it (a value that is no finding of F is left to the next
|
||||
// statement, which deletes its key when it is of a kind the writer owns).
|
||||
func NewHeldWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A]) (*HeldWriter[F, C, S, A], error) {
|
||||
if cfg.Observe != nil {
|
||||
return nil, errors.New("a held finding writer observes what it holds: it takes no Observe")
|
||||
}
|
||||
if cfg.State == nil {
|
||||
return nil, errors.New("a finding writer needs the state `findings`")
|
||||
}
|
||||
h := &HeldWriter[F, C, S, A]{held: map[string]F{}}
|
||||
keys, err := cfg.State.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, k := range keys {
|
||||
e, err := cfg.State.Get(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var f F
|
||||
if e == nil || json.Unmarshal(e.Value, &f) != nil {
|
||||
continue
|
||||
}
|
||||
if m, err := wireOf(&f); err == nil && keyOf(m) == k {
|
||||
h.held[k] = f
|
||||
}
|
||||
}
|
||||
cfg.Observe = func(context.Context) (Observed[F], error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
var o Observed[F]
|
||||
for _, f := range h.held {
|
||||
o.Findings = append(o.Findings, f)
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
w, err := NewWriter(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Writer = w
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// Write holds the finding and writes it through the writer.
|
||||
func (h *HeldWriter[F, C, S, A]) Write(ctx context.Context, finding F) error {
|
||||
m, err := wireOf(&finding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := CheckFinding(m); err != nil {
|
||||
return err
|
||||
}
|
||||
if !h.Writer.owns(str(m, "kind")) {
|
||||
return h.Writer.Write(ctx, finding) // refused by the writer, naming the kind, and held no more than it is
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.held[keyOf(m)] = finding
|
||||
h.mu.Unlock()
|
||||
return h.Writer.Write(ctx, finding)
|
||||
}
|
||||
|
||||
// Clear stops holding the finding of scope, id and kind and clears it through the writer.
|
||||
func (h *HeldWriter[F, C, S, A]) Clear(ctx context.Context, scope, id, kind, why string) error {
|
||||
h.mu.Lock()
|
||||
delete(h.held, FindingKey(scope, id, kind))
|
||||
h.mu.Unlock()
|
||||
return h.Writer.Clear(ctx, scope, id, kind, why)
|
||||
}
|
||||
@@ -463,3 +463,48 @@ func TestWriterRefusesABodyWithoutAContractField(t *testing.T) {
|
||||
t.Fatalf("a writer with no subject for findings-asked: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A part whose findings are raised as they happen holds them in a writer that observes from its own state (design 47 G A-29):
|
||||
// the findings standing in the state are seeded before it starts, a statement says what is held, Write and Clear keep the held
|
||||
// set, and an Observe of the part's own is refused.
|
||||
func TestHeldWriterStatesWhatItHoldsFromItsOwnState(t *testing.T) {
|
||||
state := &fakeState{m: map[string][]byte{}}
|
||||
raw, _ := json.Marshal(finding("seeded", "stood"))
|
||||
state.m["module.seeded.k"] = raw
|
||||
state.m["module.junk.k"] = []byte(`"not a finding"`)
|
||||
var sent []said
|
||||
cfg := testConfig{Module: "m", State: state, Node: "n1", Asked: asked,
|
||||
Publish: func(_ context.Context, subject string, data []byte, _ string) error {
|
||||
sent = append(sent, said{subject, data})
|
||||
return nil
|
||||
},
|
||||
Listen: func(context.Context, string, func([]byte)) (func(), error) { return func() {}, nil }}
|
||||
cfg.Observe = func(context.Context) (testObserved, error) { return testObserved{}, nil }
|
||||
if _, err := views.NewHeldWriter(cfg); err == nil {
|
||||
t.Fatal("a held writer took an Observe of its own")
|
||||
}
|
||||
cfg.Observe = nil
|
||||
h, err := views.NewHeldWriter(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := h.Write(ctx, finding("raised", "now")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h.Write(ctx, testFinding{Kind: "k", ID: "no-scope"}); err == nil {
|
||||
t.Fatal("a finding that breaks a rule was held")
|
||||
}
|
||||
if err := h.Clear(ctx, "module", "seeded", "k", "no longer"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sent = nil
|
||||
if err := h.State(ctx, "a1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var stated testStated
|
||||
if len(sent) != 1 || sent[0].subject != subjects.Stated || json.Unmarshal(sent[0].data, &stated) != nil ||
|
||||
len(stated.Findings) != 1 || stated.Findings[0].ID != "raised" || stated.Asked != "a1" {
|
||||
t.Fatalf("the statement says %+v after %d events", stated, len(sent))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user