From cf42bf655cd7d9e0824e42739dc373784a926735 Mon Sep 17 00:00:00 2001 From: Mateo Presa Castro Date: Sun, 7 Dec 2025 15:09:30 +0100 Subject: [PATCH 1/2] docs: improve readme and add client --- README.md | 50 +++++++++++++++++++++++++++++++-- mokv.png | Bin 0 -> 15627 bytes mokv/client.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 mokv.png create mode 100644 mokv/client.go diff --git a/README.md b/README.md index 464f9d7..f710275 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,52 @@ -# mökv + +
+mokv logo +
+
-`mökv` is a distributed, in-memory key-value store built with [`Raft`](https://github.com/hashicorp/raft) for consensus, [`Serf`](https://github.com/hashicorp/serf) for discovery, and [`gRPC`](https://github.com/grpc/grpc-go) for communication. +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Test](https://github.com/sinclairzx81/typedriver/actions/workflows/build.yml/badge.svg)](https://github.com/dynamic-calm/mokv/actions/workflows/test.yml) -Built it following the book [Distributed Services with Go](https://pragprog.com/titles/tjgo/distributed-services-with-go/) by Travis Jeffery. +
+ + + +## Example + + + +```go +package main + +import ( + "context" + + "github.com/dynamic-calm/mokv/mokv" +) + +func main() { + client, _ := mokv.NewClient("localhost:8400") + defer client.Close() + + ctx := context.Background() + + key := `59°19'14.7"N` + val := []byte(`18°03'39.0"E`) + + client.Set(ctx, key, val) + client.Get(ctx, key) + client.Delete(ctx, key) + client.GetServers(ctx) +} +``` + + + +## Overview + +mökv is a distributed key-value store built with [Raft](https://github.com/hashicorp/raft) for consensus, [Serf](https://github.com/hashicorp/serf) for discovery, and [gRPC](https://github.com/grpc/grpc-go) for communication. + +Built following the book [Distributed Services with Go](https://pragprog.com/titles/tjgo/distributed-services-with-go/) by Travis Jeffery. ## Features diff --git a/mokv.png b/mokv.png new file mode 100644 index 0000000000000000000000000000000000000000..7101c0f460c9420ac4212a7b785aac48094e8938 GIT binary patch literal 15627 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 literal 0 HcmV?d00001 diff --git a/mokv/client.go b/mokv/client.go new file mode 100644 index 0000000..9f97d89 --- /dev/null +++ b/mokv/client.go @@ -0,0 +1,75 @@ +package mokv + +import ( + "context" + + "github.com/dynamic-calm/mokv/api" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/types/known/emptypb" +) + +// Client provides access to a mökv cluster. +type Client struct { + conn *grpc.ClientConn + api api.KVClient +} + +// Server represents a node in the cluster. +type Server struct { + RpcAddr string + IsLeader bool + ID string +} + +// NewClient connects to a mökv node at the given address. +func NewClient(addr string) (*Client, error) { + conn, err := grpc.NewClient(addr, + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + return &Client{ + conn: conn, + api: api.NewKVClient(conn), + }, nil +} + +// Get retrieves a value by key. Returns nil if the key does not exist. +func (c *Client) Get(ctx context.Context, key string) ([]byte, error) { + res, err := c.api.Get(ctx, &api.GetRequest{Key: key}) + if err != nil { + return nil, err + } + return res.Value, nil +} + +// Set stores a key-value pair, replicating it across the cluster. +func (c *Client) Set(ctx context.Context, key string, value []byte) (bool, error) { + res, err := c.api.Set(ctx, &api.SetRequest{Key: key, Value: value}) + return res.Ok, err +} + +// Delete removes a key from the cluster. +func (c *Client) Delete(ctx context.Context, key string) (bool, error) { + res, err := c.api.Delete(ctx, &api.DeleteRequest{Key: key}) + return res.Ok, err +} + +// GetServers returns all nodes in the cluster. +func (c *Client) GetServers(ctx context.Context) ([]Server, error) { + res, err := c.api.GetServers(ctx, &emptypb.Empty{}) + if err != nil { + return nil, err + } + servers := make([]Server, len(res.Servers)) + for i, s := range res.Servers { + servers[i] = Server{RpcAddr: s.RpcAddr, IsLeader: s.IsLeader, ID: s.Id} + } + return servers, nil +} + +// Close releases the underlying connection. +func (c *Client) Close() error { + return c.conn.Close() +} From 872cdf3c329de97f4d3ff341daa42f458f6d5425 Mon Sep 17 00:00:00 2001 From: Mateo Presa Castro Date: Sun, 7 Dec 2025 15:32:40 +0100 Subject: [PATCH 2/2] chore: rename --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f710275..5e8999f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Test](https://github.com/sinclairzx81/typedriver/actions/workflows/build.yml/badge.svg)](https://github.com/dynamic-calm/mokv/actions/workflows/test.yml) +[![Tests](https://github.com/sinclairzx81/typedriver/actions/workflows/build.yml/badge.svg)](https://github.com/dynamic-calm/mokv/actions/workflows/test.yml)