Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ go run ./cmd/scbench -tier=laptop -json=laptop.json

CI runs the smoke suite once per side on the same GitHub runner and comments the diff on pull requests (not a merge gate). See [cmd/scbench/README.md](./cmd/scbench/README.md) and [docs/BENCHMARKS.md](./docs/BENCHMARKS.md).

### SuperCache Lab (interactive explorer)

```bash
cd examples/lab/ui && npm ci && npm run build && cd ../../..
go run ./examples/lab
# UI: http://127.0.0.1:19080/ — connect cache addrs, or -cluster for a local mesh
```

See [examples/lab/README.md](./examples/lab/README.md). Attach to a running node
or start a 3-node RF=2 demo; React playground for every keyspace mode.

### Music trending billboard (cluster demo)

```bash
Expand Down
322 changes: 322 additions & 0 deletions docs/design/2026-09-07-interactive-lab.md

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions examples/lab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# SuperCache Lab

Interactive visual explorer. Default `go run` is **UI only** — it does not start
a mesh. Connect cache gRPC addresses in the page, or opt into a local 3-node demo.

## Run

```bash
# from repo root — build UI once, then serve (no cluster)
cd examples/lab/ui && npm ci && npm run build && cd ../../..
go run ./examples/lab
# open http://127.0.0.1:19080/ → Connect 127.0.0.1:9000,… or "Local 3-node"

# attach at startup
go run ./examples/lab -addr 127.0.0.1:9000,127.0.0.1:9010

# old in-process demo mesh
go run ./examples/lab -cluster
```

Hot-reload UI (two processes):

```bash
go run ./examples/lab
cd examples/lab/ui && npm run dev # http://127.0.0.1:5173/ proxies /v1
```

Walkthrough then exit (CI):

```bash
go run ./examples/lab -hold=false
```

## What it shows

The canvas is the mesh. Chapters on the left drive a scripted scene; the playground
runs the same verbs by hand. After each call the UI polls `LocalView` on every node.

| Chapter | Keyspace | Point |
|---------|----------|--------|
| Anatomy | `cacheonly` | 3 nodes, owner of a name |
| KV write | `cacheonly` | owner ACK + async replica fill |
| LoadThrough | `loadthrough` | SoT miss, hit, singleflight |
| Tombstone | `cacheonly` | delete marker vs live |
| Bloom … VectorSet | matching mode | thin widget + wrong-verb on Set |

Lab HTTP defaults to `127.0.0.1:19080`. `-cluster` uses ephemeral cache/peer
ports (`internal/testcluster`). Mock SoT latency: `-sot-latency` (in-process only).
Remote attach talks cache gRPC only — `LocalView` owner/replica pixels need `-cluster`.

## Tests

```bash
go test ./examples/lab
```

JSON API only — Node is not required for `go test`.
107 changes: 107 additions & 0 deletions examples/lab/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package main

import (
"context"
"fmt"
"sync/atomic"
"time"

"github.com/Code0987/supercache/internal/testcluster"
"github.com/Code0987/supercache/pkg/client"
"github.com/Code0987/supercache/pkg/datasource"
"github.com/Code0987/supercache/pkg/keyspace"
)

const (
rf = 2
ksMaxBytes = 4 << 20
defaultTTL = 5 * time.Minute
negativeTTL = 10 * time.Second
)

var modeNames = []string{
"cacheonly", "loadthrough", "bloom", "set", "zset", "geo", "list",
"hash", "counter", "json", "bitmap", "hll", "topk", "cms", "vectorset",
}

type mockSoT struct {
latency time.Duration
loads atomic.Int64
}

func (m *mockSoT) Load(_ context.Context, key string) ([]byte, error) {
m.loads.Add(1)
if m.latency > 0 {
time.Sleep(m.latency)
}
return []byte(fmt.Sprintf(`{"key":%q,"src":"sot"}`, key)), nil
}

func (m *mockSoT) Loads() int64 { return m.loads.Load() }

func labKeyspaces(src datasource.DataSource) []keyspace.Config {
base := func(name string, mode keyspace.Mode) keyspace.Config {
return keyspace.Config{
Name: name, Mode: mode, MaxBytes: ksMaxBytes, TTL: defaultTTL,
ReplicationFactor: rf,
}
}
return []keyspace.Config{
base("cacheonly", keyspace.ModeCacheOnly),
{
Name: "loadthrough", Mode: keyspace.ModeLoadThrough,
MaxBytes: ksMaxBytes, TTL: defaultTTL, NegativeTTL: negativeTTL,
ReplicationFactor: rf, DataSource: src, LoadTimeout: 3 * time.Second,
},
func() keyspace.Config {
c := base("bloom", keyspace.ModeBloom)
c.BloomBits = 64
c.BloomHashes = 4
return c
}(),
base("set", keyspace.ModeSet),
base("zset", keyspace.ModeZSet),
base("geo", keyspace.ModeGeo),
base("list", keyspace.ModeList),
base("hash", keyspace.ModeHash),
base("counter", keyspace.ModeCounter),
base("json", keyspace.ModeJSON),
base("bitmap", keyspace.ModeBitmap),
base("hll", keyspace.ModeHLL),
func() keyspace.Config {
c := base("topk", keyspace.ModeTopK)
c.TopKSize = 10
return c
}(),
base("cms", keyspace.ModeCMS),
func() keyspace.Config {
c := base("vectorset", keyspace.ModeVectorSet)
c.VectorDim = 2
return c
}(),
}
}

func startMesh(src datasource.DataSource) (*testcluster.Cluster, []*client.Client, error) {
c, err := testcluster.Start(testcluster.Config{
Nodes: 3,
Keyspaces: labKeyspaces(src),
})
if err != nil {
return nil, nil, err
}
ctx := context.Background()
clis := make([]*client.Client, 0, 3)
for _, n := range c.Nodes() {
cli, err := client.Dial(ctx, n.CacheAddr)
if err != nil {
c.Close()
for _, x := range clis {
_ = x.Close()
}
return nil, nil, fmt.Errorf("dial %s: %w", n.CacheAddr, err)
}
clis = append(clis, cli)
}
return c, clis, nil
}
6 changes: 6 additions & 0 deletions examples/lab/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package main

import "embed"

//go:embed all:ui/dist
var uiDist embed.FS
Loading
Loading