From 7f8a3d6fc2767b2ae68f7884f0e39e26d8cbdd5e Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 09:00:20 +0100 Subject: [PATCH] A page's time budget is sent to the runtime, and a verb the runtime lacks is told by its code (issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/state.go | 10 +++------- go/stdio.go | 7 ++++++- go/views/tools.go | 4 ++-- go/views/views.go | 6 +++--- go/views/views_test.go | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/go/state.go b/go/state.go index bb9b532..d5cb412 100644 --- a/go/state.go +++ b/go/state.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "strings" "time" "git.novox.be/novox/mesh-sdk/go/busretry" @@ -115,15 +114,12 @@ type StatePage struct { Next string `json:"next,omitempty"` } -// Page is the current values in key order after the key `after`, as many as fit maxBytes (keys and values), at least +// Page is the current values in key order after the key `after`, as many as fit maxBytes (keys and values) or the time budget allows, at least // one: one request for many values, where Get is one request each. A runtime that does not answer it gives an error // satisfying errors.Is(err, errors.ErrUnsupported), and the reader asks Get per key instead. -func (s KeptState) Page(after string, maxBytes int) (StatePage, error) { - raw, err := s.ask("page", map[string]any{"after": after, "max_bytes": maxBytes}) +func (s KeptState) Page(after string, maxBytes int, budget time.Duration) (StatePage, error) { + raw, err := s.ask("page", map[string]any{"after": after, "max_bytes": maxBytes, "budget_ms": budget.Milliseconds()}) if err != nil { - if strings.Contains(err.Error(), "answers no mesh/state.page") { - return StatePage{}, fmt.Errorf("%w: %v", errors.ErrUnsupported, err) - } return StatePage{}, err } var p StatePage diff --git a/go/stdio.go b/go/stdio.go index 742e831..f1cf9ea 100644 --- a/go/stdio.go +++ b/go/stdio.go @@ -150,7 +150,12 @@ type refused struct { func (r *refused) Error() string { return r.message } // Is makes a compare-and-set the runtime refused because another writer was first ErrChanged. -func (r *refused) Is(target error) bool { return target == ErrChanged && r.code == CodeChanged } +func (r *refused) Is(target error) bool { + return target == ErrChanged && r.code == CodeChanged || target == errors.ErrUnsupported && r.code == CodeNoMethod +} + +// CodeNoMethod is the error code the runtime answers a verb it does not have with (JSON-RPC's method not found). +const CodeNoMethod = -32601 // ErrChanged is a compare-and-set that lost: the key was made or changed by another writer first (novox/hq // ADR 0259). Told apart by the runtime's error code, never by its words. diff --git a/go/views/tools.go b/go/views/tools.go index fc1475c..964d681 100644 --- a/go/views/tools.go +++ b/go/views/tools.go @@ -26,8 +26,8 @@ func (k KeptSource) Get(key string) (*Entry, bool, error) { } // Page is the values after a key in one request (stdio.KeptState.Page), as FirstView prefers them to Get per key. -func (k KeptSource) Page(after string, maxBytes int) ([]KeyedEntry, string, error) { - p, err := k.State.Page(after, maxBytes) +func (k KeptSource) Page(after string, maxBytes int, budget time.Duration) ([]KeyedEntry, string, error) { + p, err := k.State.Page(after, maxBytes, budget) if err != nil { return nil, "", err } diff --git a/go/views/views.go b/go/views/views.go index 0163f18..d5f0c06 100644 --- a/go/views/views.go +++ b/go/views/views.go @@ -201,10 +201,10 @@ type KeyedEntry struct { } // Pager is a Source that answers many values in one request: the values in key order after a key, as many as fit -// maxBytes and at least one, and the key to ask after for the rest. FirstView uses it when its source has it, unless +// maxBytes or the time budget and at least one, and the key to ask after for the rest. FirstView uses it when its source has it, unless // it answers errors.Is(err, errors.ErrUnsupported), when it reads per key. type Pager interface { - Page(after string, maxBytes int) ([]KeyedEntry, string, error) + Page(after string, maxBytes int, budget time.Duration) ([]KeyedEntry, string, error) } // FirstView answers one page of state from its owner's source: the mark first, so no change after the revision it @@ -226,7 +226,7 @@ func FirstView(owner, state string, values, marks Source, after string, now time } } if p, ok := values.(Pager); ok { - got, next, err := p.Page(after, PageBytes) + got, next, err := p.Page(after, PageBytes, PageBudget) if err == nil { for _, e := range got { a.put(e.Key, e.Entry) diff --git a/go/views/views_test.go b/go/views/views_test.go index 128430f..f496f51 100644 --- a/go/views/views_test.go +++ b/go/views/views_test.go @@ -676,7 +676,7 @@ func (s *pagedSource) Get(k string) (*Entry, bool, error) { return s.mapSource.Get(k) } -func (s *pagedSource) Page(after string, _ int) ([]KeyedEntry, string, error) { +func (s *pagedSource) Page(after string, _ int, _ time.Duration) ([]KeyedEntry, string, error) { if s.unsupported { return nil, "", errors.ErrUnsupported }