From 7d04e0df29437379b6d5810bf83ead40b641ccc9 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 20:39:43 +0100 Subject: [PATCH] W2.7 (format): an event kept last per subject, in a stream of its own The node-engine's running.. is kept last per subject with no age limit, which the shared events stream cannot say; the reader takes keep last-per-subject. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- go/seatdef/seatdef.go | 19 +++++++++++++++++++ go/seatdef/seatdef_test.go | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/go/seatdef/seatdef.go b/go/seatdef/seatdef.go index ddaa7c2..6a065c8 100644 --- a/go/seatdef/seatdef.go +++ b/go/seatdef/seatdef.go @@ -122,8 +122,19 @@ type Message struct { // still knows the event. Empty, the subject is the name. A part `` is the node that publishes it, which // the grants part grants a node's account alone (ADR 0363 ยง2). Subject string `json:"subject,omitempty"` + // Keep says how an event is kept when not as the family keeps the seat's others: KeepLastPerSubject, in a stream + // of its own named KeptStream, the last message of each subject and no age limit (design 47: the node-engine's + // running.., so the last build of every module on every node stays on the bus however long ago + // it was said, and a durable consumer starting from the last message per subject holds them all). + Keep string `json:"keep,omitempty"` } +// KeepLastPerSubject is an event kept last per subject with no age limit, in a stream of its own. +const KeepLastPerSubject = "last-per-subject" + +// KeptStream is the stream an event with Keep is kept in: `.`, as a family is `.events`. +func KeptStream(seat string, m Message) string { return seat + "." + m.Name } + // SubjectPattern is the event's subject after the seat's event prefix with its parts between angle brackets: the // name itself when the definition gives no subject. func (m Message) SubjectPattern() string { @@ -663,6 +674,14 @@ func checkMessages(field string, ms []Message, whole bool) Problems { ps = append(ps, Problem{where + ".name", fmt.Sprintf("%q twice", m.Name)}) } seen[m.Name] = true + switch { + case m.Keep != "" && field != "events": + ps = append(ps, Problem{where + ".keep", "only an event is kept"}) + case m.Keep != "" && m.Keep != KeepLastPerSubject: + ps = append(ps, Problem{where + ".keep", fmt.Sprintf("%q is not %s", m.Keep, KeepLastPerSubject)}) + case m.Keep != "" && len(m.Parts()) == 0 && !m.ByCaller: + ps = append(ps, Problem{where + ".keep", "an event of one subject kept last per subject is one message: give it a subject with parts"}) + } if m.Subject != "" && field != "events" { ps = append(ps, Problem{where + ".subject", "only an event carries a subject of its own"}) } else if why := checkSubject(m); why != "" { diff --git a/go/seatdef/seatdef_test.go b/go/seatdef/seatdef_test.go index ded6e2b..c406521 100644 --- a/go/seatdef/seatdef_test.go +++ b/go/seatdef/seatdef_test.go @@ -63,6 +63,12 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) { `"name":"running","subject":"running.."`, 1), "part twice"}, {"a subject on an event named by its caller", strings.Replace(whole, `"name":"created"`, `"name":"owed","by-caller":true,"subject":"owed."`, 1), "carries no subject of its own"}, + {"an event kept last per subject", strings.Replace(whole, `"name":"created"`, + `"name":"running","subject":"running..","keep":"last-per-subject"`, 1), ""}, + {"an event kept otherwise", strings.Replace(whole, `"name":"created"`, + `"name":"running","subject":"running.","keep":"forever"`, 1), `events[0].keep: "forever" is not last-per-subject`}, + {"an event of one subject kept last per subject", strings.Replace(whole, `"name":"created"`, + `"name":"running","keep":"last-per-subject"`, 1), "give it a subject with parts"}, {"an accept with dotted words", strings.Replace(whole, `"events":`, `"accepts":[{"name":"run.now","body":{}}],"events":`, 1), "accepts[0].name"}, {"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`},