Builds: design 47 G §59, §407 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
233 lines
6.3 KiB
Go
233 lines
6.3 KiB
Go
package stateobjects
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
)
|
|
|
|
// memory is a state and its object store in memory, behind the two interfaces.
|
|
type memory struct {
|
|
mu sync.Mutex
|
|
rev uint64
|
|
keys map[string]Entry
|
|
objects map[string][]byte
|
|
failPuts bool
|
|
most int
|
|
}
|
|
|
|
func newMemory() *memory { return &memory{keys: map[string]Entry{}, objects: map[string][]byte{}} }
|
|
|
|
func (m *memory) Get(key string) (*Entry, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if e, ok := m.keys[key]; ok {
|
|
return &e, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *memory) write(key string, value []byte, revision *uint64) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, has := m.keys[key]
|
|
if (revision == nil && has) || (revision != nil && (!has || e.Revision != *revision)) {
|
|
return ErrChanged
|
|
}
|
|
m.rev++
|
|
m.keys[key] = Entry{Value: value, Revision: m.rev}
|
|
return nil
|
|
}
|
|
|
|
func (m *memory) Create(key string, value []byte) error { return m.write(key, value, nil) }
|
|
func (m *memory) Update(key string, value []byte, revision uint64) error {
|
|
return m.write(key, value, &revision)
|
|
}
|
|
|
|
func (m *memory) Delete(key string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.keys, key)
|
|
return nil
|
|
}
|
|
|
|
func (m *memory) Keys() ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []string
|
|
for k := range m.keys {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memory) Put(name string, data []byte) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.failPuts {
|
|
return errors.New("the store is full")
|
|
}
|
|
m.objects[name] = data
|
|
return nil
|
|
}
|
|
|
|
func (m *memory) GetObject(name string) ([]byte, error) { return m.Object(name) }
|
|
|
|
func (m *memory) values() Values { return Values{KV: m, Objects: memObjects{m}, Store: "s-objects"} }
|
|
|
|
// Object is an object's bytes.
|
|
func (m *memory) Object(name string) ([]byte, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
d, ok := m.objects[name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("no object %s", name)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// memObjects is the memory's object store side (Put is the memory's own).
|
|
type memObjects struct{ m *memory }
|
|
|
|
func (o memObjects) MostBytes() int { return o.m.most }
|
|
func (o memObjects) Put(name string, data []byte) error { return o.m.Put(name, data) }
|
|
func (o memObjects) Get(name string) ([]byte, error) { return o.m.Object(name) }
|
|
func (o memObjects) Delete(name string) error {
|
|
o.m.mu.Lock()
|
|
defer o.m.mu.Unlock()
|
|
delete(o.m.objects, name)
|
|
return nil
|
|
}
|
|
func (o memObjects) List() ([]Object, error) {
|
|
o.m.mu.Lock()
|
|
defer o.m.mu.Unlock()
|
|
var out []Object
|
|
for n, d := range o.m.objects {
|
|
out = append(out, Object{Name: n, Size: int64(len(d))})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// runtimeOf serves one bundle tool, "run", against the memory as the tool runner would answer the state's and the object
|
|
// store's calls, and returns what the tool answered. The tool runs run in the bundle, which reaches the memory through
|
|
// stdio.State and busstate.Objects.
|
|
func runtimeOf(t *testing.T, m *memory, run func() (any, error)) any {
|
|
t.Helper()
|
|
t.Setenv(stdio.ServedAs, "owner")
|
|
inR, inW := io.Pipe()
|
|
outR, outW := io.Pipe()
|
|
go func() {
|
|
_ = stdio.ServeOn("", []stdio.Tool{{Name: "run", Run: func(map[string]any) (any, error) { return run() }}}, inR, outW)
|
|
}()
|
|
var wmu sync.Mutex
|
|
send := func(v map[string]any) {
|
|
v["jsonrpc"] = "2.0"
|
|
b, _ := json.Marshal(v)
|
|
wmu.Lock()
|
|
defer wmu.Unlock()
|
|
_, _ = inW.Write(append(b, '\n'))
|
|
}
|
|
answered := make(chan any, 1)
|
|
go func() {
|
|
lines := bufio.NewScanner(outR)
|
|
lines.Buffer(make([]byte, 1<<20), 1<<28)
|
|
for lines.Scan() {
|
|
var in struct {
|
|
ID json.RawMessage `json:"id"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
if json.Unmarshal(lines.Bytes(), &in) != nil {
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(in.Method, "mesh/") {
|
|
if string(in.ID) == "1" {
|
|
answered <- string(in.Result)
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
go func() {
|
|
res, err := m.serve(in.Method, in.Params)
|
|
a := map[string]any{"id": json.RawMessage(in.ID)}
|
|
if err != nil {
|
|
code := -32000
|
|
if errors.Is(err, ErrChanged) {
|
|
code = stdio.CodeChanged
|
|
}
|
|
a["error"] = map[string]any{"code": code, "message": err.Error()}
|
|
} else {
|
|
a["result"] = res
|
|
}
|
|
send(a)
|
|
}()
|
|
}
|
|
}()
|
|
send(map[string]any{"id": 1, "method": "tools/call", "params": map[string]any{"name": "run"}})
|
|
select {
|
|
case a := <-answered:
|
|
return a
|
|
case <-time.After(20 * time.Second):
|
|
t.Fatal("the bundle said nothing")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (m *memory) serve(method string, raw json.RawMessage) (any, error) {
|
|
var p struct {
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
Revision uint64 `json:"revision"`
|
|
Name string `json:"name"`
|
|
Data []byte `json:"data"`
|
|
Object string `json:"object"`
|
|
}
|
|
_ = json.Unmarshal(raw, &p)
|
|
switch method {
|
|
case "mesh/state.get":
|
|
e, _ := m.Get(p.Key)
|
|
if e == nil {
|
|
return nil, nil
|
|
}
|
|
return map[string]any{"key": p.Key, "value": json.RawMessage(e.Value), "revision": e.Revision}, nil
|
|
case "mesh/state.create":
|
|
return map[string]any{"revision": 1}, m.Create(p.Key, p.Value)
|
|
case "mesh/state.update":
|
|
return map[string]any{"revision": 1}, m.Update(p.Key, p.Value, p.Revision)
|
|
case "mesh/state.delete":
|
|
return buscalls.Done{}, m.Delete(p.Key)
|
|
case "mesh/state.keys":
|
|
return m.Keys()
|
|
case buscalls.ObjectsPut:
|
|
return buscalls.ObjectInfo{Name: p.Name, Size: uint64(len(p.Data))}, m.Put(p.Name, p.Data)
|
|
case buscalls.ObjectsGet:
|
|
d, err := m.Object(p.Name)
|
|
return buscalls.ObjectData{Info: buscalls.ObjectInfo{Name: p.Name}, Data: d}, err
|
|
case buscalls.ObjectsSentGet:
|
|
d, err := m.Object(p.Object)
|
|
return buscalls.ObjectData{Info: buscalls.ObjectInfo{Name: p.Object}, Data: d}, err
|
|
case buscalls.ObjectsDelete:
|
|
return buscalls.Done{}, memObjects{m}.Delete(p.Name)
|
|
case buscalls.ObjectsList:
|
|
l, _ := memObjects{m}.List()
|
|
var out []buscalls.ObjectInfo
|
|
for _, o := range l {
|
|
out = append(out, buscalls.ObjectInfo{Name: o.Name, Size: uint64(o.Size)})
|
|
}
|
|
return buscalls.ObjectsListed{Objects: out}, nil
|
|
}
|
|
return nil, fmt.Errorf("no %s", method)
|
|
}
|