package stateobjects import ( "bufio" "crypto/sha256" "encoding/hex" "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 modified map[string]time.Time most int puts map[string][]byte // a put session's chunks so far, by call } 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 if m.modified != nil { if _, ok := m.modified[name]; !ok { m.modified[name] = time.Time{} } } 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)), Modified: o.m.modified[n]}) } 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 stdio.CapabilitiesCall: return map[string]any{"words": []string{stdio.CapPutSessions}}, nil case buscalls.ObjectsPut: return buscalls.ObjectInfo{Name: p.Name, Size: uint64(len(p.Data))}, m.Put(p.Name, p.Data) case buscalls.ObjectsPutChunk: // a put session (design 47 G ยง518): the chunks are kept by call and the object is put on the last var c buscalls.ObjectsPutChunkRequest _ = json.Unmarshal(raw, &c) m.mu.Lock() if m.puts == nil { m.puts = map[string][]byte{} } m.puts[c.Call] = append(m.puts[c.Call], c.Data...) whole := m.puts[c.Call] if c.Last { delete(m.puts, c.Call) } m.mu.Unlock() if !c.Last { return buscalls.ObjectsChunkTaken{Seq: c.Seq}, nil } return buscalls.ObjectInfo{Name: c.Name, Size: uint64(len(whole))}, m.Put(c.Name, whole) case buscalls.ObjectsGet, buscalls.ObjectsSentGet: name := p.Name if method == buscalls.ObjectsSentGet { name = p.Object } d, err := m.Object(name) if err == nil && len(d) > buscalls.ChunkBytes { sum := sha256.Sum256(d) return map[string]any{"chunked": (len(d) + buscalls.ChunkBytes - 1) / buscalls.ChunkBytes, "size": len(d), "digest": "sha256:" + hex.EncodeToString(sum[:]), "call": name}, nil } return buscalls.ObjectData{Info: buscalls.ObjectInfo{Name: name}, Data: d}, err case buscalls.ObjectsGetChunk: var c buscalls.ObjectsGetChunkRequest _ = json.Unmarshal(raw, &c) d, err := m.Object(c.Call) at := int(c.Seq-1) * buscalls.ChunkBytes end := min(at+buscalls.ChunkBytes, len(d)) return buscalls.ObjectsChunk{Seq: c.Seq, Last: end == len(d), Data: d[at:end]}, 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) }