diff --git a/README.md b/README.md index 3e51de4..d8561db 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/design/2026-09-07-interactive-lab.md b/docs/design/2026-09-07-interactive-lab.md new file mode 100644 index 0000000..4cc4085 --- /dev/null +++ b/docs/design/2026-09-07-interactive-lab.md @@ -0,0 +1,322 @@ +# SuperCache Lab — interactive visual explorer + +**Status:** approved +**Branch (later):** feat/interactive-lab +**Date:** 2026-09-07 + +Revision: UI is React + Vite + TypeScript (was vanilla embed). Wide app shell is closed. +Approved in chat: start the changes (LocalView yes; first screen Anatomy). + +## Problem + +The library’s surface is large and its interesting behavior is **cluster-shaped**: + +- owner ACK + async RF fan-out +- local hit vs owner-forward +- LoadThrough + singleflight +- versioned tombstones +- one mode per keyspace, wrong verb = invalid argument +- 15 keyspace modes (KV ×2 + Bloom / Set / ZSet / Geo / List / Hash / Counter / JSON / Bitmap / HLL / TopK / CMS / VectorSet) + +What exists today does not let someone *see* that: + +| Path | What it is | Gap | +|------|------------|-----| +| `examples/billboard` | Vertical music app + small HTML | Story, not a catalog. Cluster internals are logs + `/peers`. | +| `examples/{hash,json,bitmap,hll,vecset,ratelimit}` | Print walkthroughs | One type, no UI, process exits. | +| `examples/cluster3` | Script against 3 external nodes | No visual. | +| `cmd/sc` | REPL / CLI | Text only. | +| Admin `/docs` | Swagger | Admin HTTP + proto reference, not a live mesh. | + +A newcomer cannot click a verb and watch which node owned it, which replicas filled, and what the structure looks like. + +## Non-goals + +- Not a replacement for billboard, `sc`, or admin Swagger. +- Not a production dashboard / attach-to-arbitrary-cluster debugger. +- No new Cache gRPC verbs, no proto change, no RF/TTL/hint/membership contract change. +- No Next.js, Redux, Tailwind, or component kit. React + Vite only. No WebSocket mesh protocol. +- No persistence, no real DataSource backends, no TLS in the demo. +- No animation of packet bytes or a fake “distributed debugger” that pretends to hook ApplyPut. +- Not a full Redis-compat playground. + +## Contract + +- **Public product API:** unchanged, except one optional read-only diagnostic (see [LocalView](#localview)). +- **Who stores a copy:** unchanged. Lab uses RF=2 on a 3-node in-process mesh so the canvas always has an owner, one replica, and one non-replica. +- **Clients:** existing `pkg/client` against each node’s cache port. Ingress node is user-selectable so owner-forward is visible. +- **What existing clients can assume:** nothing changes. Lab is `go run ./examples/lab` after the UI build (see [UI implementation](#ui-implementation)). + +### LocalView + +Add a **read-only** helper so the canvas can tell live / tombstone / negative / missing apart. `HasLocal` collapses those. + +```go +// pkg/engine +type LocalKind uint8 // Missing, Live, Tombstone, Negative + +type LocalView struct { + Kind LocalKind + Version uint64 + Flags uint32 + Bytes int +} + +func (e *Engine) LocalView(keyspace, key string) LocalView +``` + +Implementation is `store.Peek` + classify. No LRU touch (Peek already does that). Not on the Get-hit path. Missing keyspace → `Missing`. + +If review rejects any Engine addition, the lab falls back to `HasLocal` + `OwnerOf` and the tombstone chapter is explanatory copy only. + +## Approach + +### What it is + +**SuperCache Lab** — one process, one browser page. + +```text +go run ./examples/lab +# http://127.0.0.1:19080/ +``` + +Persistent frame is a **3-node cluster canvas**. The rest of the page is a **chapter list** (guided scenes) plus a **mode playground** (free verbs). Every action is a real Engine/client call; the picture is an observation of the mesh after the call, not a cartoon. + +```text +┌────────────┬─────────────────────────────┬──────────────────┐ +│ Chapters │ Cluster canvas │ Inspector │ +│ Anatomy │ n1 ● owner │ key / name │ +│ KV write │ n2 ○ replica (filling) │ owner, RF=2 │ +│ LoadThrough│ n3 ░ non-replica │ LocalView/node │ +│ Tombstone │ arrows: ingress → owner │ version, kind │ +│ Bloom … │ → replicas │ │ +│ VectorSet │ Event strip (this action) │ Result / error │ +├────────────┴─────────────────────────────┴──────────────────┤ +│ Playground: mode verbs, pick ingress node, sample payload │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Process layout + +Same pattern as `examples/billboard` (in-process engines + listeners), **different ports** so both can run: + +| Role | Ports | +|------|--------| +| Lab HTTP | `127.0.0.1:19080` | +| Cache gRPC | `9301–9303` | +| Peer gRPC | `9401–9403` | +| Admin | `8181–8183` | +| Gossip | `7951–7953` | + +One keyspace per mode, names = mode (`cacheonly`, `loadthrough`, `bloom`, …). `loadthrough` uses a mock DataSource with a knob for SoT latency (default 200ms). RF=2, small `MaxBytes`, short TTL on KV chapters so expiry is demoable. + +Hold-and-serve is the default (`-hold=false` exits after a smoke walkthrough, for CI). + +### Observation model (no data-path hooks) + +`Engine.Events()` is membership-only. Do **not** add ApplyPut/fan-out listeners. + +After each lab action the HTTP layer records: + +1. **Ingress** — which cache client was used. +2. **Owner** — `Engine.OwnerOf(name)` (any node; ring is shared). +3. **Per-node LocalView** — immediately, then a short poll (e.g. 5× 20ms) so async fan-out becomes visible as replica `Missing → Live`. +4. **Client result** — value / present / invalid-argument. +5. **Derived trace** (honest, labeled as inferred): + - write + ingress ≠ owner → “forwarded to owner, then ACK” + - replica LocalView flips to Live after ACK → “async fan-out” + - read + ingress Has LocalView Live → “local hit” + - CacheOnly miss on non-owner → “owner-forward” + - LoadThrough miss → “DataSource load” (mock increments a counter the UI shows) + +Slow-mo is a **UI delay between poll snapshots**, not a sleep in `pkg/engine`. + +### Chapters (guided) + +Each chapter is a scripted sequence the UI can “Run scene” *and* then leave the user in free-play on that keyspace. + +| # | Chapter | What you see | +|---|---------|----------------| +| 0 | Anatomy | 3 nodes join, `/peers`, ring gen, type a key → owner + replica set highlight | +| 1 | KV write | Put on the **non-owner**. Owner ACKs; replica fills on the next poll; non-replica stays empty. Get from each node: local vs forward. | +| 2 | LoadThrough | Cold Get → SoT log + latency. Second Get is a hit. Stampede button (N concurrent Gets) → one SoT load. | +| 3 | Tombstone | Delete; all nodes LocalView=Tombstone (or miss if LocalView rejected). Put a new value → new version Live. Copy explains delayed ApplyPut cannot resurrect. | +| 4–N | One chapter per structured mode | Tiny visual + the verbs that matter (table below). Each chapter ends with “wrong verb on this keyspace → invalid argument”. | + +### Mode widgets (free-play + chapter) + +Thin, mode-specific visuals. Not a second product. + +| Mode | Widget | Verbs exposed | +|------|--------|----------------| +| CacheOnly | key/value editor | Get, Put, Delete | +| LoadThrough | same + SoT hit counter | Get, Delete (Put optional pin) | +| Bloom | chip list + Test maybe/no | BloomAdd, BloomTest, Delete | +| Set | member list | SetAdd, SetRemove, SetContains, SetCard, SetMembers, Delete | +| ZSet | ranked table | ZAdd, ZRem, ZScore, ZCard, ZRange, Delete | +| Geo | 2D plot + radius circle | GeoAdd, GeoRem, GeoPos, GeoRadius, GeoDist, Delete | +| List | horizontal cells | LPush, RPush, LPop, RPop, LLen, LRange, Delete | +| Hash | field table | HSet, HGet, HDel, HGetAll, HLen, Delete | +| Counter | big number | Incr, CounterGet, Delete | +| JSON | path + pretty tree | JsonSet, JsonGet, JsonDel, Delete | +| Bitmap | bit grid (first 64 bits) | BitSet, BitGet, BitCount, Delete | +| HLL | estimate vs exact UI set | HLLAdd, HLLCount, Delete | +| TopK | bar chart K=10 | TopKAdd, TopKList, Delete | +| CMS | query one item | CMSIncr, CMSQuery, Delete | +| VectorSet | 2D vectors + query arrow | VAdd, VRem, VSim, VCard, VEmb, Delete | + +Copy on each widget: **when to use this mode** (one sentence) and **what it is not** (e.g. TopK is observations, not ZAdd scores). + +### HTTP surface (lab process only) + +Not part of node admin. JSON in/out. + +| Route | Role | +|-------|------| +| `GET /` | Embedded Vite `index.html` (+ hashed assets under `/assets/`) | +| `GET /v1/cluster` | nodes, addrs, ring gen, keyspaces | +| `GET /v1/view?ks=&key=` | owner + LocalView per node | +| `POST /v1/op` | `{ks, op, name, args, via}` → run client verb, return result + before/after views + inferred trace | +| `POST /v1/scene/:id` | run a chapter’s scripted steps, return the step list | +| `POST /v1/reset` | Delete known demo names (or recreate keyspaces) | + +No SSE in v1. The client polls `/v1/view` during slow-mo. Revisit SSE only if polling feels wrong. + +### UI implementation + +React SPA in `examples/lab/ui`, served by the lab process. + +| Piece | Choice | +|-------|--------| +| UI | React 18 | +| Language | TypeScript | +| Bundler | Vite | +| Routing | none — one page; chapter is React state (and `?chapter=` for refresh) | +| Data | `fetch` to `/v1/*`; no React Query / Redux | +| Style | one `app.css`, billboard tokens (`#0b0f14` / `#121821`), wide app shell | +| Charts | DOM/CSS node cards + CSS/SVG arrows. Geo + VectorSet use inline SVG. No Canvas2D/WebGL, no chart library | +| Embed | `//go:embed ui/dist` on the lab HTTP server | + +```text +examples/lab/ + main.go # cluster + HTTP + embed + http.go + ui/ + package.json + vite.config.ts # proxy /v1 → :19080 in dev + src/App.tsx + src/cluster/Canvas.tsx + src/chapters/... + src/widgets/... # one component per mode + dist/ # Vite output, embedded +``` + +**Run modes** + +```text +# hot-reload UI (two processes) +go run ./examples/lab # API + cluster on :19080 +cd examples/lab/ui && npm run dev # Vite :5173, proxies /v1 + +# single process (what README leads with) +cd examples/lab/ui && npm ci && npm run build +go run ./examples/lab # serves embedded dist at / +``` + +Vite `server.proxy`: `/v1` → `http://127.0.0.1:19080`. CORS is not required if every browser call goes through that proxy or same-origin embed. + +**`go test ./examples/lab` does not need Node.** Tests hit JSON routes only. A missing `ui/dist` still starts the cluster; `GET /` returns a short “build the UI” page so API tests stay green. + +**Commit `ui/dist`** so `go run ./examples/lab` works without Node after clone. Rebuild and commit dist when the React tree changes. `package-lock.json` is committed; `node_modules` is gitignored. `//go:embed` needs at least `ui/dist/index.html` in git (Vite output, or a stub until the first build). + +Lab HTTP serves `GET /` and hashed Vite assets from the embed FS. No extra SPA fallback routes — there is no client router. + +Component map (implement from this, do not invent a second tree): + +| Component | Role | +|-----------|------| +| `App` | shell, chapter state, last op / views | +| `ChapterNav` | list in the left rail | +| `ClusterCanvas` | 3 `NodeCard`s + inferred arrows | +| `Inspector` | owner, RF, per-node LocalView, result | +| `Playground` | ingress picker + current mode widget | +| `widgets/*` | one file per mode from the table above | + +### Tests (after approval) + +| Test | Package | Asserts | +|------|---------|---------| +| `TestLabHTTPCluster` | `examples/lab` | process starts 3 nodes; `GET /v1/cluster` returns 3 peers + all mode keyspaces | +| `TestLabOpPutReplicaFill` | `examples/lab` | Put via non-owner; owner LocalView Live immediately; replica Live within poll window; non-replica not Live | +| `TestLabWrongVerb` | `examples/lab` | `Get` on `ModeSet` → 400 / invalid argument | +| `TestLabLoadThroughSingleflight` | `examples/lab` | N concurrent Gets on cold key → mock SoT loads == 1 | +| `TestLocalViewKinds` | `pkg/engine` | live / tombstone / negative / missing (only if LocalView is in scope) | +| `TestLabHoldFalse` | `examples/lab` | `-hold=false` walkthrough exits 0 | + +No new scbench cells. No Playwright / browser tests in this PR — React is covered by building `ui/dist` and the Go JSON tests. A missing or stub `ui/dist` must not fail `go test ./examples/lab`. + +### Bench risk + +- **Hot path?** No, unless LocalView is mistakenly called from Get. It must use Peek only. +- Shared smoke / Get-hit allocs: **must not move**. LocalView is a new symbol, unused by Get/Put. +- Lab tests start a 3-node mesh (same cost class as `examples/hash`). Keep them in `go test ./examples/lab`, not the engine package. + +### Docs + +- `examples/lab/README.md` — `npm run dev` vs `npm run build` + `go run`, ports, chapter list. +- One paragraph + link from root `README.md` (next to billboard). +- Do not rewrite PLAN.md. + +## Rejected alternatives + +| Idea | Why not | +|------|---------| +| Grow billboard HTML into a catalog | Mixes a product story with a lab; billboard already has a job. | +| Attach Lab to a running `supercache-node` | No LocalView/HasLocal over admin HTTP today; would force a new admin API. | +| Instrument ApplyPut / fan-out with Engine events | Data-path change, Get-hit risk, more than a demo needs. | +| Vanilla one-file HTML (billboard style) | User wants React. The canvas + 15 widgets will not stay readable as a string const. | +| Next.js / SSR | Lab is a local single page talking to `:19080`. No SEO, no server render. | +| Tailwind / component kit | Extra design system for one example. Billboard tokens + one CSS file. | +| One chapter / one PR | Leaves a half-lab; SuperCache is one design → one PR. Widgets stay thin. | +| Fake the cluster in JS | Teaches the wrong thing the moment fan-out or RF is surprising. | + +## Key Decisions + +1. **New example, not a billboard fork** — catalog vs story. +2. **Cluster canvas is the frame** — SuperCache’s differentiator is the mesh, not another hash-map form. +3. **Observe after the call** — `OwnerOf` + `HasLocal` / `LocalView` + poll. No data-path event bus. +4. **RF=2 on N=3** — owner / replica / non-replica always visible. +5. **All modes in this PR, thin widgets** — otherwise it does not explore the library. +6. **Optional LocalView** — only Engine addition; read-only Peek wrapper. +7. **React + Vite, embedded `ui/dist`** — UI is a real SPA; `go run` still serves one origin after `npm run build`. Tests stay Go-only. + +## Open Questions + +1. **LocalView in this PR?** Recommended yes (tombstone chapter is honest). Alternative: HasLocal only, no `pkg/engine` change. +2. **Default first screen?** Recommended chapter 0 (Anatomy) auto-run on load, then stay on the canvas. Alternative: mode gallery grid first. +3. **Visual density?** Closed: wide React app shell (canvas + inspector). Billboard’s article layout does not fit 15 widgets. + +## Tests (write these first, after approval) + +See table above. New `examples/lab` tests should fail to compile until the HTTP server exists. `TestLocalViewKinds` should fail until the method exists. + +## Bench risk + +See above. Gate: no Get-hit / StoreGetHit alloc increase; no shared smoke cell worse than 10%. Lab itself is not a CI smoke cell. + +## PR Plan + +One design → one PR. + +### PR 1 — `feat: SuperCache Lab interactive explorer` + +- **Title:** SuperCache Lab: interactive cluster + mode explorer +- **Branch:** `feat/interactive-lab` +- **Files:** + - `docs/design/2026-09-07-interactive-lab.md` (this file, status → approved) + - `pkg/engine/status.go` (+ test) — `LocalView` if approved + - `examples/lab/` — cluster, HTTP, embed of `ui/dist`, scenes, tests, README + - `examples/lab/ui/` — Vite + React 18 + TypeScript (widgets, canvas, lockfile, committed `dist/`) + - `README.md` — one link under examples +- **Dependencies:** none +- **Description:** In-process 3-node lab, observation API, guided chapters, thin widget per mode. No proto/RF/Get-Put contract change. diff --git a/examples/lab/README.md b/examples/lab/README.md new file mode 100644 index 0000000..0529550 --- /dev/null +++ b/examples/lab/README.md @@ -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`. diff --git a/examples/lab/cluster.go b/examples/lab/cluster.go new file mode 100644 index 0000000..a4138f9 --- /dev/null +++ b/examples/lab/cluster.go @@ -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 +} diff --git a/examples/lab/embed.go b/examples/lab/embed.go new file mode 100644 index 0000000..65d52af --- /dev/null +++ b/examples/lab/embed.go @@ -0,0 +1,6 @@ +package main + +import "embed" + +//go:embed all:ui/dist +var uiDist embed.FS diff --git a/examples/lab/http.go b/examples/lab/http.go new file mode 100644 index 0000000..0a69698 --- /dev/null +++ b/examples/lab/http.go @@ -0,0 +1,538 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "strings" + "sync" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/Code0987/supercache/internal/testcluster" + "github.com/Code0987/supercache/pkg/bloom" + "github.com/Code0987/supercache/pkg/client" + "github.com/Code0987/supercache/pkg/engine" +) + +type labConfig struct { + HTTPAddr string + SoTLatency time.Duration + InProcess bool // start the 3-node demo mesh + Addrs []string // dial these cache gRPC addrs instead +} + +type backendNode struct { + ID string + CacheAddr string + PeerAddr string + Client *client.Client + Engine *engine.Engine +} + +// Lab is the explorer HTTP server, optionally attached to a mesh. +type Lab struct { + mu sync.Mutex + Cluster *testcluster.Cluster + owned bool + nodes []backendNode + SoT *mockSoT + sotLat time.Duration + HTTP *http.Server + Addr string + ln net.Listener +} + +func startLab(cfg labConfig) (*Lab, error) { + if cfg.HTTPAddr == "" { + cfg.HTTPAddr = "127.0.0.1:19080" + } + if cfg.SoTLatency == 0 { + cfg.SoTLatency = 200 * time.Millisecond + } + l := &Lab{SoT: &mockSoT{latency: cfg.SoTLatency}, sotLat: cfg.SoTLatency} + if cfg.InProcess { + if err := l.startInProcess(); err != nil { + return nil, err + } + } else if len(cfg.Addrs) > 0 { + if err := l.dialRemote(cfg.Addrs); err != nil { + return nil, err + } + } + ln, err := net.Listen("tcp", cfg.HTTPAddr) + if err != nil { + l.Close() + return nil, err + } + l.ln = ln + l.Addr = ln.Addr().String() + l.HTTP = &http.Server{Handler: l.handler(), ReadHeaderTimeout: 5 * time.Second} + go func() { _ = l.HTTP.Serve(ln) }() + return l, nil +} + +func (l *Lab) Close() { + if l == nil { + return + } + if l.HTTP != nil { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + _ = l.HTTP.Shutdown(ctx) + cancel() + } + if l.ln != nil { + _ = l.ln.Close() + } + l.dropBackend() +} + +func (l *Lab) handler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/v1/cluster", l.handleCluster) + mux.HandleFunc("/v1/connect", l.handleConnect) + mux.HandleFunc("/v1/disconnect", l.handleDisconnect) + mux.HandleFunc("/v1/view", l.handleView) + mux.HandleFunc("/v1/bloom", l.handleBloom) + mux.HandleFunc("/v1/op", l.handleOp) + mux.HandleFunc("/v1/scene/", l.handleScene) + mux.HandleFunc("/v1/reset", l.handleReset) + mux.Handle("/", uiHandler()) + return mux +} + +func uiHandler() http.Handler { + sub, err := fs.Sub(uiDist, "ui/dist") + if err != nil { + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "ui embed missing — npm --prefix examples/lab/ui run build", http.StatusNotFound) + }) + } + return http.FileServer(http.FS(sub)) +} + +func (l *Lab) handleCluster(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + writeJSON(w, http.StatusOK, l.clusterJSON()) +} + +func (l *Lab) handleConnect(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var body struct { + Addrs any `json:"addrs"` + InProcess bool `json:"in_process"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&body); err != nil && err != io.EOF { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) + return + } + var err error + if body.InProcess { + err = l.startInProcess() + } else { + err = l.dialRemote(parseAddrs(body.Addrs)) + } + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, l.clusterJSON()) +} + +func (l *Lab) handleDisconnect(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + l.dropBackend() + writeJSON(w, http.StatusOK, l.clusterJSON()) +} + +func (l *Lab) handleView(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + ks := r.URL.Query().Get("ks") + key := r.URL.Query().Get("key") + if ks == "" || key == "" { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": "ks and key required"}) + return + } + writeJSON(w, http.StatusOK, l.snapshot(ks, key)) +} + +func (l *Lab) handleBloom(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + ks := r.URL.Query().Get("ks") + name := r.URL.Query().Get("name") + item := r.URL.Query().Get("item") + if ks == "" { + ks = "bloom" + } + if name == "" { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": "name required"}) + return + } + writeJSON(w, http.StatusOK, l.bloomViz(ks, name, item)) +} + +func (l *Lab) handleOp(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req opReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) + return + } + resp, code := l.runOp(r.Context(), req) + writeJSON(w, code, resp) +} + +func (l *Lab) handleScene(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + id := strings.TrimPrefix(r.URL.Path, "/v1/scene/") + id = strings.Trim(id, "/") + out, err := l.runScene(r.Context(), id) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, out) +} + +func (l *Lab) handleReset(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + names := append([]string{}, demoNames...) + var body struct { + Names []string `json:"names"` + } + _ = json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&body) + names = append(names, body.Names...) + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + cli, _, err := l.pick("") + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) + return + } + for _, ks := range modeNames { + for _, name := range names { + _ = cli.Delete(ctx, ks, name) + } + } + writeJSON(w, http.StatusOK, map[string]any{"ok": true, "names": names}) +} + +func (l *Lab) pick(via string) (*client.Client, backendNode, error) { + l.mu.Lock() + defer l.mu.Unlock() + if len(l.nodes) == 0 { + return nil, backendNode{}, errors.New("not connected — set cache gRPC addresses") + } + if via == "" { + return l.nodes[0].Client, l.nodes[0], nil + } + for _, n := range l.nodes { + if n.ID == via || n.CacheAddr == via { + return n.Client, n, nil + } + } + return nil, backendNode{}, errors.New("unknown via node") +} + +func (l *Lab) snapshot(ks, key string) map[string]any { + l.mu.Lock() + nodes := append([]backendNode(nil), l.nodes...) + l.mu.Unlock() + ownerID := "" + rfEff := rf + var ringGen uint64 + if len(nodes) > 0 && nodes[0].Engine != nil { + if o, ok := nodes[0].Engine.OwnerOf(key); ok { + ownerID = o.ID + } + ringGen = nodes[0].Engine.RingGeneration() + for _, s := range nodes[0].Engine.KeySpaceSnapshots() { + if s.Name == ks { + rfEff = s.ReplicationFactor + break + } + } + } + out := make([]map[string]any, 0, len(nodes)) + for _, n := range nodes { + kind, ver, flags, bytes := "missing", uint64(0), uint32(0), 0 + role := "other" + if n.Engine != nil { + v := n.Engine.LocalView(ks, key) + kind, ver, flags, bytes = v.Kind.String(), v.Version, v.Flags, v.Bytes + switch { + case n.ID == ownerID: + role = "owner" + case v.Kind == engine.LocalLive: + role = "replica" + } + } + out = append(out, map[string]any{ + "id": n.ID, + "cache": n.CacheAddr, + "kind": kind, + "version": ver, + "flags": flags, + "bytes": bytes, + "role": role, + }) + } + return map[string]any{ + "ks": ks, + "key": key, + "owner": ownerID, + "rf": rfEff, + "ring_gen": ringGen, + "nodes": out, + } +} + +func (l *Lab) clusterJSON() map[string]any { + l.mu.Lock() + defer l.mu.Unlock() + outNodes := make([]map[string]any, 0, len(l.nodes)) + var ringGen uint64 + var snaps []engine.KeySpaceSnapshot + addrs := make([]string, 0, len(l.nodes)) + mode := "disconnected" + if l.owned && l.Cluster != nil { + mode = "in_process" + } else if len(l.nodes) > 0 { + mode = "remote" + } + for _, n := range l.nodes { + node := map[string]any{"id": n.ID, "cache": n.CacheAddr, "peer": n.PeerAddr, "ready": n.Client != nil} + if n.Engine != nil { + node["peers"] = n.Engine.Peers() + node["ready"] = n.Engine.Ready() + node["ring"] = n.Engine.RingGeneration() + ringGen = n.Engine.RingGeneration() + if snaps == nil { + snaps = n.Engine.KeySpaceSnapshots() + } + } + outNodes = append(outNodes, node) + addrs = append(addrs, n.CacheAddr) + } + if snaps == nil { + snaps = []engine.KeySpaceSnapshot{} + } + lat := l.sotLat.String() + loads := int64(0) + if l.SoT != nil { + loads = l.SoT.Loads() + lat = l.SoT.latency.String() + } + return map[string]any{ + "mode": mode, + "connected": len(l.nodes) > 0, + "addrs": addrs, + "nodes": outNodes, + "keyspaces": snaps, + "ring_gen": ringGen, + "rf": rf, + "sot_loads": loads, + "sot_latency": lat, + } +} + +func (l *Lab) startInProcess() error { + sot := &mockSoT{latency: l.sotLat} + cl, clis, err := startMesh(sot) + if err != nil { + return err + } + nodes := make([]backendNode, 0, len(clis)) + for i, n := range cl.Nodes() { + nodes = append(nodes, backendNode{ + ID: n.ID, CacheAddr: n.CacheAddr, PeerAddr: n.PeerAddr, + Client: clis[i], Engine: n.Engine, + }) + } + l.replaceBackend(nodes, cl, sot, true) + return nil +} + +func (l *Lab) dialRemote(addrs []string) error { + if len(addrs) == 0 { + return errors.New("addrs required (cache gRPC host:port, comma-separated)") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + nodes := make([]backendNode, 0, len(addrs)) + for i, addr := range addrs { + cli, err := client.Dial(ctx, addr) + if err != nil { + for _, n := range nodes { + _ = n.Client.Close() + } + return fmt.Errorf("dial %s: %w", addr, err) + } + nodes = append(nodes, backendNode{ + ID: fmt.Sprintf("n%d", i), CacheAddr: addr, Client: cli, + }) + } + l.replaceBackend(nodes, nil, &mockSoT{latency: l.sotLat}, false) + return nil +} + +func (l *Lab) replaceBackend(nodes []backendNode, cl *testcluster.Cluster, sot *mockSoT, owned bool) { + l.mu.Lock() + oldNodes, oldCl, oldOwned := l.nodes, l.Cluster, l.owned + l.nodes = nodes + l.Cluster = cl + l.owned = owned + if sot != nil { + l.SoT = sot + } + l.mu.Unlock() + closeBackends(oldNodes, oldCl, oldOwned) +} + +func (l *Lab) dropBackend() { + l.mu.Lock() + oldNodes, oldCl, oldOwned := l.nodes, l.Cluster, l.owned + l.nodes = nil + l.Cluster = nil + l.owned = false + l.SoT = &mockSoT{latency: l.sotLat} + l.mu.Unlock() + closeBackends(oldNodes, oldCl, oldOwned) +} + +func closeBackends(nodes []backendNode, cl *testcluster.Cluster, owned bool) { + for _, n := range nodes { + if n.Client != nil { + _ = n.Client.Close() + } + } + if owned && cl != nil { + cl.Close() + } +} + +func parseAddrs(v any) []string { + var raw []string + switch t := v.(type) { + case string: + raw = strings.Split(t, ",") + case []any: + for _, x := range t { + if s, ok := x.(string); ok { + raw = append(raw, s) + } + } + case []string: + raw = t + } + out := make([]string, 0, len(raw)) + for _, s := range raw { + s = strings.TrimSpace(s) + if s != "" { + out = append(out, s) + } + } + return out +} + +func (l *Lab) bloomViz(ks, name, item string) map[string]any { + l.mu.Lock() + nodes := append([]backendNode(nil), l.nodes...) + l.mu.Unlock() + m, k := 64, 4 + var raw []byte + present := false + for _, n := range nodes { + if n.Engine == nil { + continue + } + bits, bm, bk, ok := n.Engine.BloomDump(ks, name) + if bm > 0 { + m, k = bm, bk + } + if ok { + raw, present = bits, true + break + } + } + on := make([]bool, m) + if present { + for i := 0; i < m; i++ { + if i/8 < len(raw) && raw[i/8]&(1<<(i%8)) != 0 { + on[i] = true + } + } + } + pos := []int{} + maybe := false + if item != "" { + pos = bloom.Indexes(m, k, []byte(item)) + if present && len(pos) > 0 { + maybe = true + for _, p := range pos { + if p < 0 || p >= len(on) || !on[p] { + maybe = false + break + } + } + } + } + return map[string]any{ + "m": m, "k": k, "present": present, "bits": on, + "positions": pos, "maybe": maybe, "item": item, "name": name, + } +} + +func (l *Lab) sotLoads() int64 { + if l == nil || l.SoT == nil { + return 0 + } + return l.SoT.Loads() +} + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(v) +} + +func isInvalidArg(err error) bool { + if err == nil { + return false + } + if st, ok := status.FromError(err); ok && st.Code() == codes.InvalidArgument { + return true + } + return strings.Contains(strings.ToLower(err.Error()), "invalid argument") +} diff --git a/examples/lab/lab_test.go b/examples/lab/lab_test.go new file mode 100644 index 0000000..ec88861 --- /dev/null +++ b/examples/lab/lab_test.go @@ -0,0 +1,297 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sync" + "testing" + "time" +) + +func TestLabHTTPCluster(t *testing.T) { + lab := startTestLab(t) + body := getJSON(t, lab, "/v1/cluster") + if n := len(asSlice(body["nodes"])); n != 3 { + t.Fatalf("nodes=%d body=%v", n, body) + } + want := []string{ + "cacheonly", "loadthrough", "bloom", "set", "zset", "geo", "list", + "hash", "counter", "json", "bitmap", "hll", "topk", "cms", "vectorset", + } + got := map[string]bool{} + for _, raw := range asSlice(body["keyspaces"]) { + ks, _ := raw.(map[string]any) + name, _ := ks["name"].(string) + got[name] = true + } + for _, name := range want { + if !got[name] { + t.Fatalf("missing keyspace %q in %v", name, body["keyspaces"]) + } + } +} + +func TestLabOpPutReplicaFill(t *testing.T) { + lab := startTestLab(t) + key := "lab-rf-" + fmt.Sprint(time.Now().UnixNano()) + view := getJSON(t, lab, "/v1/view?ks=cacheonly&key="+key) + owner, _ := view["owner"].(string) + if owner == "" { + t.Fatalf("no owner: %v", view) + } + via := otherNode(view, owner) + resp := postJSON(t, lab, "/v1/op", map[string]any{ + "ks": "cacheonly", "op": "put", "name": key, "via": via, + "args": map[string]any{"value": "hello"}, + }) + if ok, _ := resp["ok"].(bool); !ok { + t.Fatalf("put: %v", resp) + } + deadline := time.Now().Add(2 * time.Second) + var liveOwner, liveReplica, liveOther int + for time.Now().Before(deadline) { + v := getJSON(t, lab, "/v1/view?ks=cacheonly&key="+key) + liveOwner, liveReplica, liveOther = classifyLive(v, owner) + if liveOwner == 1 && liveReplica == 1 && liveOther == 0 { + return + } + time.Sleep(20 * time.Millisecond) + } + t.Fatalf("replica fill: ownerLive=%d replicaLive=%d otherLive=%d owner=%s", liveOwner, liveReplica, liveOther, owner) +} + +func TestLabWrongVerb(t *testing.T) { + lab := startTestLab(t) + code, body := postJSONStatus(t, lab, "/v1/op", map[string]any{ + "ks": "set", "op": "get", "name": "flags", + "args": map[string]any{}, + }) + if code != http.StatusBadRequest { + t.Fatalf("status %d body=%s", code, body) + } + var resp map[string]any + if err := json.Unmarshal(body, &resp); err != nil { + t.Fatal(err) + } + if inv, _ := resp["invalid_argument"].(bool); !inv { + t.Fatalf("want invalid_argument: %v", resp) + } +} + +func TestLabLoadThroughSingleflight(t *testing.T) { + lab := startTestLab(t) + key := "stampede-" + fmt.Sprint(time.Now().UnixNano()) + view := getJSON(t, lab, "/v1/view?ks=loadthrough&key="+key) + owner, _ := view["owner"].(string) + before, _ := getJSON(t, lab, "/v1/cluster")["sot_loads"].(float64) + const n = 16 + var wg sync.WaitGroup + wg.Add(n) + for i := 0; i < n; i++ { + go func() { + defer wg.Done() + postJSON(t, lab, "/v1/op", map[string]any{ + "ks": "loadthrough", "op": "get", "name": key, "via": owner, + }) + }() + } + wg.Wait() + cl := getJSON(t, lab, "/v1/cluster") + loads, _ := cl["sot_loads"].(float64) + if int(loads-before) != 1 { + t.Fatalf("sot_loads delta=%v (before=%v after=%v) want 1", loads-before, before, loads) + } +} + +func TestLabStartsDisconnected(t *testing.T) { + lab, err := startLab(labConfig{HTTPAddr: "127.0.0.1:0"}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(lab.Close) + body := getJSON(t, lab, "/v1/cluster") + if n := len(asSlice(body["nodes"])); n != 0 { + t.Fatalf("default run should not start a mesh, nodes=%d", n) + } + if on, _ := body["connected"].(bool); on { + t.Fatalf("want disconnected: %v", body) + } + code, raw := postJSONStatus(t, lab, "/v1/op", map[string]any{ + "ks": "cacheonly", "op": "get", "name": "k", + }) + if code != http.StatusBadRequest { + t.Fatalf("op without backend: %d %s", code, raw) + } +} + +func TestLabConnectRemote(t *testing.T) { + src := startTestLab(t) // in-process mesh we attach to + info := getJSON(t, src, "/v1/cluster") + var addrs []string + for _, raw := range asSlice(info["nodes"]) { + n, _ := raw.(map[string]any) + if a, _ := n["cache"].(string); a != "" { + addrs = append(addrs, a) + } + } + if len(addrs) != 3 { + t.Fatalf("src addrs: %v", addrs) + } + + lab, err := startLab(labConfig{HTTPAddr: "127.0.0.1:0"}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(lab.Close) + got := postJSON(t, lab, "/v1/connect", map[string]any{"addrs": addrs}) + if n := len(asSlice(got["nodes"])); n != 3 { + t.Fatalf("connected nodes=%d %v", n, got) + } + if mode, _ := got["mode"].(string); mode != "remote" { + t.Fatalf("mode=%v", got["mode"]) + } + key := "attach-" + fmt.Sprint(time.Now().UnixNano()) + put := postJSON(t, lab, "/v1/op", map[string]any{ + "ks": "cacheonly", "op": "put", "name": key, + "args": map[string]any{"value": "via-remote"}, + }) + if ok, _ := put["ok"].(bool); !ok { + t.Fatalf("remote put: %v", put) + } + got = postJSON(t, lab, "/v1/disconnect", map[string]any{}) + if on, _ := got["connected"].(bool); on { + t.Fatalf("still connected: %v", got) + } +} + +func TestLabBloomGrid(t *testing.T) { + lab := startTestLab(t) + put := postJSON(t, lab, "/v1/op", map[string]any{ + "ks": "bloom", "op": "bloomadd", "name": "users", + "args": map[string]any{"item": "alice"}, + }) + if ok, _ := put["ok"].(bool); !ok { + t.Fatalf("bloomadd: %v", put) + } + viz := getJSON(t, lab, "/v1/bloom?ks=bloom&name=users&item=alice") + if on, _ := viz["present"].(bool); !on { + t.Fatalf("present: %v", viz) + } + if m, _ := viz["m"].(float64); m != 64 { + t.Fatalf("m=%v want 64", viz["m"]) + } + pos := asSlice(viz["positions"]) + if len(pos) != 4 { + t.Fatalf("positions=%v", pos) + } + if maybe, _ := viz["maybe"].(bool); !maybe { + t.Fatalf("alice should be maybe: %v", viz) + } + bits := asSlice(viz["bits"]) + if len(bits) != 64 { + t.Fatalf("bits len %d", len(bits)) + } +} + +func TestLabHoldFalse(t *testing.T) { + var buf bytes.Buffer + if err := runWalkthrough(&buf); err != nil { + t.Fatalf("%v\n%s", err, buf.String()) + } + if !strings.Contains(buf.String(), "OK: SuperCache Lab walkthrough passed") { + t.Fatalf("missing OK line:\n%s", buf.String()) + } +} + +func startTestLab(t *testing.T) *Lab { + t.Helper() + lab, err := startLab(labConfig{HTTPAddr: "127.0.0.1:0", SoTLatency: 30 * time.Millisecond, InProcess: true}) + if err != nil { + t.Fatalf("startLab: %v", err) + } + t.Cleanup(lab.Close) + return lab +} + +func getJSON(t *testing.T, lab *Lab, path string) map[string]any { + t.Helper() + resp, err := http.Get("http://" + lab.Addr + path) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + t.Fatalf("GET %s → %d %s", path, resp.StatusCode, b) + } + var out map[string]any + if err := json.Unmarshal(b, &out); err != nil { + t.Fatalf("json %s: %v %s", path, err, b) + } + return out +} + +func postJSON(t *testing.T, lab *Lab, path string, payload map[string]any) map[string]any { + t.Helper() + code, body := postJSONStatus(t, lab, path, payload) + if code != 200 { + t.Fatalf("POST %s → %d %s", path, code, body) + } + var out map[string]any + if err := json.Unmarshal(body, &out); err != nil { + t.Fatalf("json: %v %s", err, body) + } + return out +} + +func postJSONStatus(t *testing.T, lab *Lab, path string, payload map[string]any) (int, []byte) { + t.Helper() + raw, err := json.Marshal(payload) + if err != nil { + t.Fatal(err) + } + resp, err := http.Post("http://"+lab.Addr+path, "application/json", bytes.NewReader(raw)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return resp.StatusCode, b +} + +func asSlice(v any) []any { + s, _ := v.([]any) + return s +} + +func otherNode(view map[string]any, owner string) string { + for _, raw := range asSlice(view["nodes"]) { + n, _ := raw.(map[string]any) + id, _ := n["id"].(string) + if id != "" && id != owner { + return id + } + } + return "" +} + +func classifyLive(view map[string]any, owner string) (ownerLive, replicaLive, otherLive int) { + for _, raw := range asSlice(view["nodes"]) { + n, _ := raw.(map[string]any) + id, _ := n["id"].(string) + kind, _ := n["kind"].(string) + if kind != "live" { + continue + } + if id == owner { + ownerLive++ + continue + } + replicaLive++ + } + return +} diff --git a/examples/lab/main.go b/examples/lab/main.go new file mode 100644 index 0000000..88227ea --- /dev/null +++ b/examples/lab/main.go @@ -0,0 +1,69 @@ +// SuperCache Lab: interactive cluster + mode explorer. +// +// go run ./examples/lab +// go run ./examples/lab -hold=false # walkthrough then exit (CI) +// cd examples/lab/ui && npm run dev # Vite :5173 proxies /v1 +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + var ( + httpAddr = flag.String("http", "127.0.0.1:19080", "lab HTTP listen address") + sotLatency = flag.Duration("sot-latency", 200*time.Millisecond, "mock LoadThrough SoT latency") + inProcess = flag.Bool("cluster", false, "start in-process 3-node demo mesh") + addrs = flag.String("addr", "", "comma-separated cache gRPC addrs (no in-process mesh)") + runDemo = flag.Bool("demo", true, "run scripted walkthrough after cluster is up") + hold = flag.Bool("hold", true, "keep serving after demo until Ctrl+C") + ) + flag.Parse() + + logger := log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds) + logger.Println("SuperCache Lab") + logger.Printf("config: http=%s cluster=%v addr=%q sot_latency=%s demo=%v hold=%v", + *httpAddr, *inProcess, *addrs, *sotLatency, *runDemo, *hold) + + if !*hold && *httpAddr == "127.0.0.1:19080" && !*inProcess && *addrs == "" { + if err := runWalkthrough(os.Stdout); err != nil { + logger.Fatalf("walkthrough: %v", err) + } + return + } + + lab, err := startLab(labConfig{ + HTTPAddr: *httpAddr, SoTLatency: *sotLatency, + InProcess: *inProcess, Addrs: parseAddrs(*addrs), + }) + if err != nil { + logger.Fatalf("start: %v", err) + } + defer lab.Close() + logger.Printf("lab HTTP http://%s (Vite dev: examples/lab/ui npm run dev)", lab.Addr) + info := lab.clusterJSON() + logger.Printf(" backend mode=%v connected=%v", info["mode"], info["connected"]) + if !*inProcess && *addrs == "" { + logger.Printf(" no mesh started — connect cache gRPC addrs in the UI or pass -addr / -cluster") + } + + if *runDemo && !*hold { + if err := runWalkthrough(os.Stdout); err != nil { + logger.Fatalf("walkthrough: %v", err) + } + } + + if !*hold { + return + } + logger.Printf("open http://%s/", lab.Addr) + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + <-ch + logger.Println("shutting down") +} diff --git a/examples/lab/op.go b/examples/lab/op.go new file mode 100644 index 0000000..7517086 --- /dev/null +++ b/examples/lab/op.go @@ -0,0 +1,388 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/Code0987/supercache/pkg/client" + "github.com/Code0987/supercache/pkg/engine" +) + +type opReq struct { + KS string `json:"ks"` + Op string `json:"op"` + Name string `json:"name"` + Via string `json:"via"` + Args json.RawMessage `json:"args"` +} + +type opArgs struct { + Value string `json:"value"` + Item string `json:"item"` + Member string `json:"member"` + Field string `json:"field"` + Path string `json:"path"` + A string `json:"a"` + B string `json:"b"` + Score float64 `json:"score"` + Delta int64 `json:"delta"` + Offset uint64 `json:"offset"` + Bit *bool `json:"bit"` + Lon float64 `json:"lon"` + Lat float64 `json:"lat"` + Radius float64 `json:"radius"` + Limit int `json:"limit"` + Start int `json:"start"` + Stop *int `json:"stop"` + End *int `json:"end"` + K int `json:"k"` + N uint64 `json:"n"` + Vec []float32 `json:"vec"` +} + +func (l *Lab) runOp(ctx context.Context, req opReq) (map[string]any, int) { + if req.KS == "" || req.Op == "" || req.Name == "" { + return map[string]any{"ok": false, "error": "ks, op, and name required", "invalid_argument": true}, http.StatusBadRequest + } + cli, node, err := l.pick(req.Via) + if err != nil { + return map[string]any{"ok": false, "error": err.Error()}, http.StatusBadRequest + } + var args opArgs + if len(req.Args) > 0 { + _ = json.Unmarshal(req.Args, &args) + } + before := l.snapshot(req.KS, req.Name) + sot0 := l.sotLoads() + result, err := dispatch(ctx, cli, req.KS, req.Op, req.Name, args) + after := l.snapshot(req.KS, req.Name) + // Poll so async fan-out can flip replica Missing → Live before we return. + deadline := time.Now().Add(120 * time.Millisecond) + for time.Now().Before(deadline) { + if replicaFilled(before, after) { + break + } + time.Sleep(20 * time.Millisecond) + after = l.snapshot(req.KS, req.Name) + } + sotDelta := l.sotLoads() - sot0 + owner, _ := after["owner"].(string) + out := map[string]any{ + "ok": err == nil, + "result": result, + "via": node.ID, + "owner": owner, + "before": before, + "after": after, + "trace": inferTrace(req.Op, node.ID, owner, before, after, sotDelta), + "sot_loads": l.sotLoads(), + "sot_delta": sotDelta, + "invalid_argument": false, + } + if err != nil { + out["ok"] = false + out["error"] = err.Error() + if isInvalidArg(err) { + out["invalid_argument"] = true + return out, http.StatusBadRequest + } + if err == client.ErrNotFound { + out["error"] = "not found" + return out, http.StatusOK + } + return out, http.StatusOK + } + return out, http.StatusOK +} + +func replicaFilled(before, after map[string]any) bool { + b := nodesByID(before) + a := nodesByID(after) + for id, nv := range a { + if nv["role"] == "owner" { + continue + } + if nv["kind"] == "live" && b[id]["kind"] != "live" { + return true + } + } + return false +} + +func nodesByID(snap map[string]any) map[string]map[string]any { + out := map[string]map[string]any{} + raw, _ := snap["nodes"].([]any) + if raw == nil { + if typed, ok := snap["nodes"].([]map[string]any); ok { + for _, n := range typed { + id, _ := n["id"].(string) + out[id] = n + } + return out + } + } + for _, v := range raw { + n, _ := v.(map[string]any) + id, _ := n["id"].(string) + out[id] = n + } + return out +} + +func inferTrace(op, via, owner string, before, after map[string]any, sotDelta int64) []string { + var t []string + write := isWriteOp(op) + read := !write + if write && via != "" && owner != "" && via != owner { + t = append(t, "inferred: forwarded to owner, then ACK") + } + if write { + b, a := nodesByID(before), nodesByID(after) + for id, nv := range a { + if nv["kind"] == "live" && b[id]["kind"] != "live" && id != owner { + t = append(t, "inferred: async fan-out") + break + } + } + } + if read { + b := nodesByID(before) + if via != "" && b[via]["kind"] == "live" { + t = append(t, "inferred: local hit") + } else if op == "get" && via != owner && owner != "" { + t = append(t, "inferred: owner-forward") + } + } + if sotDelta > 0 { + t = append(t, fmt.Sprintf("inferred: DataSource load (×%d)", sotDelta)) + } + if len(t) == 0 { + t = append(t, "inferred: observed after the call (no extra hops visible)") + } + return t +} + +func isWriteOp(op string) bool { + switch strings.ToLower(op) { + case "put", "delete", "bloomadd", "sadd", "srem", "zadd", "zrem", + "geoadd", "georem", "lpush", "rpush", "lpop", "rpop", + "hset", "hdel", "incr", "jsonset", "jsondel", + "bitset", "hlladd", "topkadd", "cmsincr", "vadd", "vrem": + return true + } + return false +} + +func dispatch(ctx context.Context, cli *client.Client, ks, op, name string, a opArgs) (any, error) { + stop := -1 + if a.Stop != nil { + stop = *a.Stop + } + end := -1 + if a.End != nil { + end = *a.End + } + bit := true + if a.Bit != nil { + bit = *a.Bit + } + if a.Delta == 0 && strings.ToLower(op) == "incr" { + a.Delta = 1 + } + if a.N == 0 { + a.N = 1 + } + if a.K <= 0 { + a.K = 3 + } + if a.Path == "" { + a.Path = "$" + } + switch strings.ToLower(op) { + case "get": + v, err := cli.Get(ctx, ks, name) + if err != nil { + return nil, err + } + return map[string]any{"value": string(v)}, nil + case "put": + return map[string]any{"acked": true}, cli.Put(ctx, ks, name, []byte(a.Value)) + case "delete", "del": + return map[string]any{"acked": true}, cli.Delete(ctx, ks, name) + case "bloomadd": + return map[string]any{"acked": true}, cli.BloomAdd(ctx, ks, name, []byte(a.Item)) + case "bloomtest": + maybe, err := cli.BloomTest(ctx, ks, name, []byte(a.Item)) + return map[string]any{"maybe": maybe}, err + case "sadd", "setadd": + return map[string]any{"acked": true}, cli.SetAdd(ctx, ks, name, []byte(a.Item)) + case "srem", "setremove": + return map[string]any{"acked": true}, cli.SetRemove(ctx, ks, name, []byte(a.Item)) + case "sismember", "setcontains": + ok, err := cli.SetContains(ctx, ks, name, []byte(a.Item)) + return map[string]any{"present": ok}, err + case "scard", "setcard": + n, err := cli.SetCard(ctx, ks, name) + return map[string]any{"card": n}, err + case "smembers", "setmembers": + ms, err := cli.SetMembers(ctx, ks, name) + return map[string]any{"members": asStrings(ms)}, err + case "zadd": + return map[string]any{"acked": true}, cli.ZAdd(ctx, ks, name, []byte(a.Member), a.Score) + case "zrem": + return map[string]any{"acked": true}, cli.ZRem(ctx, ks, name, []byte(a.Member)) + case "zscore": + sc, ok, err := cli.ZScore(ctx, ks, name, []byte(a.Member)) + return map[string]any{"score": sc, "present": ok}, err + case "zcard": + n, err := cli.ZCard(ctx, ks, name) + return map[string]any{"card": n}, err + case "zrange": + ms, err := cli.ZRange(ctx, ks, name, a.Start, stop) + return zMembersJSON(ms), err + case "geoadd": + return map[string]any{"acked": true}, cli.GeoAdd(ctx, ks, name, []byte(a.Member), a.Lon, a.Lat) + case "georem": + return map[string]any{"acked": true}, cli.GeoRem(ctx, ks, name, []byte(a.Member)) + case "geopos": + lon, lat, ok, err := cli.GeoPos(ctx, ks, name, []byte(a.Member)) + return map[string]any{"lon": lon, "lat": lat, "present": ok}, err + case "geocard": + n, err := cli.GeoCard(ctx, ks, name) + return map[string]any{"card": n}, err + case "geodist": + d, ok, err := cli.GeoDist(ctx, ks, name, []byte(a.A), []byte(a.B)) + return map[string]any{"meters": d, "present": ok}, err + case "georadius": + ms, err := cli.GeoRadius(ctx, ks, name, a.Lon, a.Lat, a.Radius, a.Limit) + out := make([]map[string]any, 0, len(ms)) + for _, m := range ms { + out = append(out, map[string]any{"member": string(m.Member), "lon": m.Lon, "lat": m.Lat, "dist_meters": m.Dist}) + } + return map[string]any{"members": out}, err + case "lpush": + return map[string]any{"acked": true}, cli.LPush(ctx, ks, name, []byte(a.Item)) + case "rpush": + return map[string]any{"acked": true}, cli.RPush(ctx, ks, name, []byte(a.Item)) + case "lpop": + v, ok, err := cli.LPop(ctx, ks, name) + return map[string]any{"value": string(v), "present": ok}, err + case "rpop": + v, ok, err := cli.RPop(ctx, ks, name) + return map[string]any{"value": string(v), "present": ok}, err + case "llen": + n, err := cli.LLen(ctx, ks, name) + return map[string]any{"len": n}, err + case "lindex": + v, ok, err := cli.LIndex(ctx, ks, name, a.Start) + return map[string]any{"value": string(v), "present": ok}, err + case "lrange": + ms, err := cli.LRange(ctx, ks, name, a.Start, stop) + return map[string]any{"items": asStrings(ms)}, err + case "hset": + return map[string]any{"acked": true}, cli.HSet(ctx, ks, name, []byte(a.Field), []byte(a.Value)) + case "hget": + v, ok, err := cli.HGet(ctx, ks, name, []byte(a.Field)) + return map[string]any{"value": string(v), "present": ok}, err + case "hdel": + return map[string]any{"acked": true}, cli.HDel(ctx, ks, name, []byte(a.Field)) + case "hexists": + ok, err := cli.HExists(ctx, ks, name, []byte(a.Field)) + return map[string]any{"present": ok}, err + case "hlen": + n, err := cli.HLen(ctx, ks, name) + return map[string]any{"len": n}, err + case "hgetall": + fs, err := cli.HGetAll(ctx, ks, name) + pairs := make([]map[string]string, 0, len(fs)) + for _, f := range fs { + pairs = append(pairs, map[string]string{"field": string(f.Field), "value": string(f.Value)}) + } + return map[string]any{"fields": pairs}, err + case "incr": + n, err := cli.Incr(ctx, ks, name, a.Delta) + return map[string]any{"value": n}, err + case "cget", "counterget": + n, ok, err := cli.CounterGet(ctx, ks, name) + return map[string]any{"value": n, "present": ok}, err + case "jsonset": + return map[string]any{"acked": true}, cli.JsonSet(ctx, ks, name, a.Path, []byte(a.Value)) + case "jsonget": + v, ok, err := cli.JsonGet(ctx, ks, name, a.Path) + return map[string]any{"value": json.RawMessage(v), "present": ok, "raw": string(v)}, err + case "jsondel": + return map[string]any{"acked": true}, cli.JsonDel(ctx, ks, name, a.Path) + case "bitset": + return map[string]any{"acked": true}, cli.BitSet(ctx, ks, name, a.Offset, bit) + case "bitget": + b, ok, err := cli.BitGet(ctx, ks, name, a.Offset) + return map[string]any{"bit": b, "present": ok}, err + case "bitcount": + n, err := cli.BitCount(ctx, ks, name, a.Start, end) + return map[string]any{"count": n}, err + case "bitpos": + pos, ok, err := cli.BitPos(ctx, ks, name, bit, a.Start, end) + return map[string]any{"pos": pos, "found": ok}, err + case "hlladd": + return map[string]any{"acked": true}, cli.HLLAdd(ctx, ks, name, []byte(a.Item)) + case "hllcount": + n, ok, err := cli.HLLCount(ctx, ks, name) + return map[string]any{"count": n, "present": ok}, err + case "topkadd": + return map[string]any{"acked": true}, cli.TopKAdd(ctx, ks, name, []byte(a.Item)) + case "topklist": + es, ok, err := cli.TopKList(ctx, ks, name) + rows := make([]map[string]any, 0, len(es)) + for _, e := range es { + rows = append(rows, map[string]any{"item": string(e.Item), "count": e.Count}) + } + return map[string]any{"entries": rows, "present": ok}, err + case "cmsincr": + return map[string]any{"acked": true}, cli.CMSIncr(ctx, ks, name, []byte(a.Item), a.N) + case "cmsquery": + n, ok, err := cli.CMSQuery(ctx, ks, name, []byte(a.Item)) + return map[string]any{"count": n, "present": ok}, err + case "vadd": + return map[string]any{"acked": true}, cli.VAdd(ctx, ks, name, []byte(a.Member), a.Vec) + case "vrem": + return map[string]any{"acked": true}, cli.VRem(ctx, ks, name, []byte(a.Member)) + case "vsim": + hits, err := cli.VSim(ctx, ks, name, a.Vec, a.K) + rows := make([]map[string]any, 0, len(hits)) + for _, h := range hits { + rows = append(rows, map[string]any{"member": string(h.Member), "score": h.Score}) + } + return map[string]any{"hits": rows}, err + case "vcard": + n, ok, err := cli.VCard(ctx, ks, name) + return map[string]any{"card": n, "present": ok}, err + case "vdim": + n, ok, err := cli.VDim(ctx, ks, name) + return map[string]any{"dim": n, "present": ok}, err + case "vemb": + v, ok, err := cli.VEmb(ctx, ks, name, []byte(a.Member)) + return map[string]any{"vec": v, "present": ok}, err + default: + return nil, fmt.Errorf("%w: unknown op %q", engine.ErrInvalidArgument, op) + } +} + +func asStrings(in [][]byte) []string { + out := make([]string, len(in)) + for i, b := range in { + out[i] = string(b) + } + return out +} + +func zMembersJSON(ms []client.ZMember) map[string]any { + out := make([]map[string]any, 0, len(ms)) + for _, m := range ms { + out = append(out, map[string]any{"member": string(m.Member), "score": m.Score}) + } + return map[string]any{"members": out} +} diff --git a/examples/lab/scene.go b/examples/lab/scene.go new file mode 100644 index 0000000..e82b12b --- /dev/null +++ b/examples/lab/scene.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" +) + +var demoNames = []string{ + "session", "chart", "users", "flags", "board", "places", "inbox", + "profile", "rl", "doc", "seen", "uniques", "hot", "freq", "items", +} + +type sceneStep struct { + Title string `json:"title"` + Op opReq `json:"op"` + Resp map[string]any `json:"resp"` +} + +func (l *Lab) runScene(ctx context.Context, id string) (map[string]any, error) { + ops, blurb, err := sceneOps(id) + if err != nil { + return nil, err + } + steps := make([]sceneStep, 0, len(ops)) + for _, req := range ops { + resp, _ := l.runOp(ctx, req) + steps = append(steps, sceneStep{Title: req.Op + " " + req.Name, Op: req, Resp: resp}) + } + return map[string]any{ + "id": id, + "blurb": blurb, + "steps": steps, + }, nil +} + +func sceneOps(id string) ([]opReq, string, error) { + switch id { + case "anatomy", "": + return []opReq{ + mustOp("cacheonly", "put", "session", "", map[string]any{"value": "hello-lab"}), + mustOp("cacheonly", "get", "session", "", nil), + }, "3-node mesh, RF=2. Type a key in the inspector to see the owner.", nil + case "kv-write": + return []opReq{ + mustOp("cacheonly", "put", "session", "", map[string]any{"value": "v1"}), + mustOp("cacheonly", "get", "session", "", nil), + }, "Put ACKs on the owner; a replica fills asynchronously; the third node stays empty.", nil + case "loadthrough": + return []opReq{ + mustOp("loadthrough", "get", "chart", "", nil), + mustOp("loadthrough", "get", "chart", "", nil), + }, "First Get misses to the mock SoT; the second is a hit. Stampede coalesces to one load.", nil + case "tombstone": + return []opReq{ + mustOp("cacheonly", "put", "session", "", map[string]any{"value": "old"}), + mustOp("cacheonly", "delete", "session", "", nil), + mustOp("cacheonly", "put", "session", "", map[string]any{"value": "new"}), + }, "Delete installs a versioned tombstone so a delayed ApplyPut cannot resurrect the old value.", nil + case "bloom": + return []opReq{ + mustOp("bloom", "bloomadd", "users", "", map[string]any{"item": "alice"}), + mustOp("bloom", "bloomtest", "users", "", map[string]any{"item": "alice"}), + mustOp("bloom", "bloomtest", "users", "", map[string]any{"item": "bob"}), + }, "Approximate membership. maybe=false means definitely not.", nil + case "set": + return []opReq{ + mustOp("set", "sadd", "flags", "", map[string]any{"item": "dark_mode"}), + mustOp("set", "sismember", "flags", "", map[string]any{"item": "dark_mode"}), + mustOp("set", "smembers", "flags", "", nil), + }, "Exact membership. Wrong verb (Get) is invalid argument.", nil + case "zset": + return []opReq{ + mustOp("zset", "zadd", "board", "", map[string]any{"member": "alice", "score": 100}), + mustOp("zset", "zadd", "board", "", map[string]any{"member": "bob", "score": 80}), + mustOp("zset", "zrange", "board", "", map[string]any{"start": 0, "stop": -1}), + }, "Scored sorted set. Observations belong in TopK, not here.", nil + case "geo": + return []opReq{ + mustOp("geo", "geoadd", "places", "", map[string]any{"member": "shop", "lon": -74.0, "lat": 40.7}), + mustOp("geo", "georadius", "places", "", map[string]any{"lon": -74.0, "lat": 40.7, "radius": 20000, "limit": 10}), + }, "WGS84 points + haversine radius. Not an embedding space.", nil + case "list": + return []opReq{ + mustOp("list", "rpush", "inbox", "", map[string]any{"item": "event1"}), + mustOp("list", "rpush", "inbox", "", map[string]any{"item": "event2"}), + mustOp("list", "lrange", "inbox", "", map[string]any{"start": 0, "stop": -1}), + }, "Ordered list. Replicas get a full snapshot after each mutate.", nil + case "hash": + return []opReq{ + mustOp("hash", "hset", "profile", "", map[string]any{"field": "email", "value": "a@b"}), + mustOp("hash", "hset", "profile", "", map[string]any{"field": "name", "value": "Ada"}), + mustOp("hash", "hgetall", "profile", "", nil), + }, "Per-field LWW. Concurrent field writers do not clobber each other.", nil + case "counter": + return []opReq{ + mustOp("counter", "incr", "rl", "", map[string]any{"delta": 1}), + mustOp("counter", "cget", "rl", "", nil), + }, "Owner-serialized int64. Live 0 stays until Delete.", nil + case "json": + return []opReq{ + mustOp("json", "jsonset", "doc", "", map[string]any{"path": "$.name", "value": `"Ada"`}), + mustOp("json", "jsonget", "doc", "", map[string]any{"path": "$"}), + }, "Path set/get on one document. Arrays are not auto-created.", nil + case "bitmap": + return []opReq{ + mustOp("bitmap", "bitset", "seen", "", map[string]any{"offset": 0, "bit": true}), + mustOp("bitmap", "bitget", "seen", "", map[string]any{"offset": 0}), + mustOp("bitmap", "bitcount", "seen", "", map[string]any{"start": 0, "end": -1}), + }, "Packed Redis-order bits. Clearing a bit is not Delete.", nil + case "hll": + return []opReq{ + mustOp("hll", "hlladd", "uniques", "", map[string]any{"item": "a"}), + mustOp("hll", "hlladd", "uniques", "", map[string]any{"item": "b"}), + mustOp("hll", "hllcount", "uniques", "", nil), + }, "Approximate distinct count. Items are hashed, not stored.", nil + case "topk": + return []opReq{ + mustOp("topk", "topkadd", "hot", "", map[string]any{"item": "t001"}), + mustOp("topk", "topkadd", "hot", "", map[string]any{"item": "t001"}), + mustOp("topk", "topkadd", "hot", "", map[string]any{"item": "t002"}), + mustOp("topk", "topklist", "hot", "", nil), + }, "Space-Saving heavy hitters. Writes are observations, not ZAdd scores.", nil + case "cms": + return []opReq{ + mustOp("cms", "cmsincr", "freq", "", map[string]any{"item": "t003", "n": 5}), + mustOp("cms", "cmsquery", "freq", "", map[string]any{"item": "t003"}), + }, "Count-Min frequency of any named item, including TopK evictions.", nil + case "vectorset": + return []opReq{ + mustOp("vectorset", "vadd", "items", "", map[string]any{"member": "east", "vec": []float32{1, 0}}), + mustOp("vectorset", "vadd", "items", "", map[string]any{"member": "north", "vec": []float32{0, 1}}), + mustOp("vectorset", "vsim", "items", "", map[string]any{"vec": []float32{1, 0.05}, "k": 2}), + }, "Brute-force K-NN. Metric is a keyspace knob, not a query argument.", nil + default: + return nil, "", fmt.Errorf("unknown scene %q", id) + } +} + +func mustOp(ks, op, name, via string, args map[string]any) opReq { + var raw json.RawMessage + if args != nil { + raw, _ = json.Marshal(args) + } + return opReq{KS: ks, Op: op, Name: name, Via: via, Args: raw} +} diff --git a/examples/lab/ui/.gitignore b/examples/lab/ui/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/examples/lab/ui/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/lab/ui/dist/assets/index-BOjbhdCx.css b/examples/lab/ui/dist/assets/index-BOjbhdCx.css new file mode 100644 index 0000000..88828ca --- /dev/null +++ b/examples/lab/ui/dist/assets/index-BOjbhdCx.css @@ -0,0 +1 @@ +:root{font-family:ui-sans-serif,system-ui,sans-serif;color:#e8eef7;background:#0b0f14;line-height:1.4}*{box-sizing:border-box}html,body,#root{margin:0;height:100%}button,input,select,textarea{font:inherit;color:inherit}button{background:#38bdf8;color:#0b0f14;border:0;padding:.4rem .75rem;border-radius:8px;font-weight:600;cursor:pointer}button.ghost{background:transparent;color:#e8eef7;border:1px solid #334155}button:disabled{opacity:.5;cursor:default}input,select,textarea{background:#0f172a;border:1px solid #334155;border-radius:6px;padding:.35rem .5rem}input::placeholder,textarea::placeholder{color:#64748b;opacity:1}code{background:#1e293b;padding:.1rem .35rem;border-radius:4px;font-size:.9em}.shell{display:grid;grid-template-columns:180px 1fr 580px;grid-template-rows:auto 1fr;grid-template-areas:"top top top" "nav canvas sidebar";height:100%;min-height:0}.top{grid-area:top;display:flex;align-items:center;flex-wrap:wrap;gap:.5rem .65rem;padding:.55rem 1rem;border-bottom:1px solid #1e293b;background:#121821}.top h1{font-size:1.05rem;margin:0;letter-spacing:-.02em;white-space:nowrap}.muted{color:#8b9bb4;font-size:.85rem}.top .grow{flex:1}.top-addrs{flex:1 1 18rem;min-width:12rem}.nav{grid-area:nav;overflow:auto;border-right:1px solid #1e293b;padding:.5rem 0}.nav button{display:block;width:calc(100% - .8rem);margin:.15rem .4rem;text-align:left;background:transparent;color:#cbd5e1;font-weight:500}.nav button.active{background:#1e293b;color:#7dd3fc}.canvas{grid-area:canvas;padding:1rem;min-width:0;display:flex;flex-direction:column;gap:.75rem}.nodes{display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem}.node{background:#121821;border:1px solid #1e293b;border-radius:12px;padding:.9rem 1rem;min-height:140px}.node.owner{border-color:#38bdf8;box-shadow:0 0 0 1px #38bdf833}.node.replica{border-color:#34d399}.node.tombstone{border-color:#fbbf24}.node.negative{border-color:#c084fc}.node .id{font-weight:700}.node .meta{color:#8b9bb4;font-size:.8rem;margin-top:.35rem}.kind{display:inline-block;margin-top:.5rem;font-size:.75rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:.15rem .4rem;border-radius:999px;background:#1e293b}.kind.live{background:#064e3b;color:#6ee7b7}.kind.missing{color:#94a3b8}.kind.tombstone{background:#78350f;color:#fde68a}.kind.negative{background:#4c1d95;color:#ddd6fe}.trace{background:#0f172a;border-radius:8px;padding:.6rem .8rem;font-size:.85rem;color:#93c5fd;min-height:2.4rem;overflow-wrap:anywhere;word-break:break-word}.sidebar{grid-area:sidebar;display:grid;grid-template-columns:1fr 1fr;min-height:0;overflow:hidden;border-left:1px solid #1e293b;background:#121821}.inspector{padding:.85rem;font-size:.9rem;min-width:0;min-height:0;overflow-x:hidden;overflow-y:auto;overflow-wrap:anywhere;word-break:break-word}.inspector h2,.play>.section-label{font-size:.75rem;text-transform:uppercase;letter-spacing:.06em;color:#8b9bb4;margin:0 0 .5rem}.inspector dl{margin:0 0 1rem}.inspector dt{color:#8b9bb4;font-size:.75rem}.inspector dd{margin:0 0 .45rem}.play{padding:.85rem 1rem 1rem;min-width:0;min-height:0;overflow:auto;border-right:1px solid #1e293b}.play h2{margin:0 0 .35rem;font-size:1rem}.note{color:#8b9bb4;font-size:.85rem;margin:0 0 .7rem}.row{display:flex;flex-wrap:wrap;gap:.45rem;align-items:center;margin-bottom:.45rem}.row input,.row select{min-width:0;flex:1 1 8rem}.stack{display:flex;flex-direction:column;gap:.25rem;margin-bottom:.55rem;color:#8b9bb4;font-size:.75rem}.stack input{width:100%}.pair{display:grid;grid-template-columns:1fr 1fr;gap:.45rem}.pair .stack{margin-bottom:.55rem}.stack-actions{display:flex;flex-direction:column;gap:.4rem;margin-bottom:.65rem}.stack-actions button{width:100%}.chips{display:flex;flex-wrap:wrap;gap:.35rem}.chip{background:#1e293b;color:#e8eef7;font-weight:500;padding:.25rem .55rem;border-radius:999px}.chip.on{background:#0e7490;color:#ecfeff}.bloom-hit{color:#6ee7b7;font-weight:700}.bloom-miss{color:#fca5a5;font-weight:700}.bits{display:grid;grid-template-columns:repeat(8,1fr);gap:4px;max-width:100%}.bit{display:flex;align-items:center;justify-content:center;aspect-ratio:1;border-radius:4px;background:#1e293b;color:#64748b;border:1px solid #334155;padding:0;font-size:.65rem;font-weight:600}.bit.on{background:#38bdf8;color:#0b0f14;border-color:#7dd3fc}.bit.probe{box-shadow:0 0 0 2px #fbbf24}.bit.probe:not(.on){color:#fde68a;border-color:#fbbf24}.plot{width:100%;max-width:220px;height:220px;background:#0f172a;border-radius:8px;border:1px solid #1e293b}.bars{display:flex;align-items:flex-end;gap:6px;height:80px}.bar{width:28px;background:#38bdf8;border-radius:4px 4px 0 0;min-height:4px}pre.result{background:#0f172a;padding:.6rem;border-radius:8px;overflow-x:hidden;overflow-y:auto;font-size:11px;max-height:none;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word} diff --git a/examples/lab/ui/dist/assets/index-CkCcC2Cr.js b/examples/lab/ui/dist/assets/index-CkCcC2Cr.js new file mode 100644 index 0000000..1b44998 --- /dev/null +++ b/examples/lab/ui/dist/assets/index-CkCcC2Cr.js @@ -0,0 +1,40 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerPolicy&&(o.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?o.credentials="include":l.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}})();function uc(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Gu={exports:{}},rl={},Xu={exports:{}},M={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Jn=Symbol.for("react.element"),sc=Symbol.for("react.portal"),ac=Symbol.for("react.fragment"),cc=Symbol.for("react.strict_mode"),fc=Symbol.for("react.profiler"),dc=Symbol.for("react.provider"),pc=Symbol.for("react.context"),hc=Symbol.for("react.forward_ref"),mc=Symbol.for("react.suspense"),vc=Symbol.for("react.memo"),gc=Symbol.for("react.lazy"),Fi=Symbol.iterator;function yc(e){return e===null||typeof e!="object"?null:(e=Fi&&e[Fi]||e["@@iterator"],typeof e=="function"?e:null)}var Zu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ju=Object.assign,qu={};function cn(e,t,n){this.props=e,this.context=t,this.refs=qu,this.updater=n||Zu}cn.prototype.isReactComponent={};cn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};cn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function bu(){}bu.prototype=cn.prototype;function Bo(e,t,n){this.props=e,this.context=t,this.refs=qu,this.updater=n||Zu}var Wo=Bo.prototype=new bu;Wo.constructor=Bo;Ju(Wo,cn.prototype);Wo.isPureReactComponent=!0;var Ui=Array.isArray,es=Object.prototype.hasOwnProperty,Ho={current:null},ts={key:!0,ref:!0,__self:!0,__source:!0};function ns(e,t,n){var r,l={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)es.call(t,r)&&!ts.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1>>1,J=E[K];if(0>>1;Kl(xl,O))xtl(rr,xl)?(E[K]=rr,E[xt]=O,K=xt):(E[K]=xl,E[wt]=O,K=wt);else if(xtl(rr,O))E[K]=rr,E[xt]=O,K=xt;else break e}}return T}function l(E,T){var O=E.sortIndex-T.sortIndex;return O!==0?O:E.id-T.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var i=Date,u=i.now();e.unstable_now=function(){return i.now()-u}}var s=[],c=[],h=1,v=null,m=3,k=!1,y=!1,x=!1,P=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,f=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function p(E){for(var T=n(c);T!==null;){if(T.callback===null)r(c);else if(T.startTime<=E)r(c),T.sortIndex=T.expirationTime,t(s,T);else break;T=n(c)}}function g(E){if(x=!1,p(E),!y)if(n(s)!==null)y=!0,kl(S);else{var T=n(c);T!==null&&wl(g,T.startTime-E)}}function S(E,T){y=!1,x&&(x=!1,d(z),z=-1),k=!0;var O=m;try{for(p(T),v=n(s);v!==null&&(!(v.expirationTime>T)||E&&!R());){var K=v.callback;if(typeof K=="function"){v.callback=null,m=v.priorityLevel;var J=K(v.expirationTime<=T);T=e.unstable_now(),typeof J=="function"?v.callback=J:v===n(s)&&r(s),p(T)}else r(s);v=n(s)}if(v!==null)var nr=!0;else{var wt=n(c);wt!==null&&wl(g,wt.startTime-T),nr=!1}return nr}finally{v=null,m=O,k=!1}}var j=!1,_=null,z=-1,V=5,C=-1;function R(){return!(e.unstable_now()-CE||125K?(E.sortIndex=O,t(c,E),n(s)===null&&E===n(c)&&(x?(d(z),z=-1):x=!0,wl(g,O-K))):(E.sortIndex=J,t(s,E),y||k||(y=!0,kl(S))),E},e.unstable_shouldYield=R,e.unstable_wrapCallback=function(E){var T=m;return function(){var O=m;m=T;try{return E.apply(this,arguments)}finally{m=O}}}})(us);is.exports=us;var Lc=is.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Tc=L,Se=Lc;function w(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Xl=Object.prototype.hasOwnProperty,Rc=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Ai={},Vi={};function Oc(e){return Xl.call(Vi,e)?!0:Xl.call(Ai,e)?!1:Rc.test(e)?Vi[e]=!0:(Ai[e]=!0,!1)}function Mc(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Ic(e,t,n,r){if(t===null||typeof t>"u"||Mc(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ce(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var ne={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ne[e]=new ce(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ne[t]=new ce(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ne[e]=new ce(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ne[e]=new ce(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ne[e]=new ce(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ne[e]=new ce(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ne[e]=new ce(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ne[e]=new ce(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ne[e]=new ce(e,5,!1,e.toLowerCase(),null,!1,!1)});var Ko=/[\-:]([a-z])/g;function Yo(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Ko,Yo);ne[t]=new ce(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Ko,Yo);ne[t]=new ce(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Ko,Yo);ne[t]=new ce(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ne[e]=new ce(e,1,!1,e.toLowerCase(),null,!1,!1)});ne.xlinkHref=new ce("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ne[e]=new ce(e,1,!1,e.toLowerCase(),null,!0,!0)});function Go(e,t,n,r){var l=ne.hasOwnProperty(t)?ne[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var s=` +`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{jl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?xn(e):""}function Dc(e){switch(e.tag){case 5:return xn(e.type);case 16:return xn("Lazy");case 13:return xn("Suspense");case 19:return xn("SuspenseList");case 0:case 2:case 15:return e=Nl(e.type,!1),e;case 11:return e=Nl(e.type.render,!1),e;case 1:return e=Nl(e.type,!0),e;default:return""}}function bl(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ut:return"Fragment";case Ft:return"Portal";case Zl:return"Profiler";case Xo:return"StrictMode";case Jl:return"Suspense";case ql:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case cs:return(e.displayName||"Context")+".Consumer";case as:return(e._context.displayName||"Context")+".Provider";case Zo:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Jo:return t=e.displayName||null,t!==null?t:bl(e.type)||"Memo";case et:t=e._payload,e=e._init;try{return bl(e(t))}catch{}}return null}function Fc(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return bl(t);case 8:return t===Xo?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function ht(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ds(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Uc(e){var t=ds(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ir(e){e._valueTracker||(e._valueTracker=Uc(e))}function ps(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ds(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Or(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function eo(e,t){var n=t.checked;return H({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Wi(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=ht(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function hs(e,t){t=t.checked,t!=null&&Go(e,"checked",t,!1)}function to(e,t){hs(e,t);var n=ht(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?no(e,t.type,n):t.hasOwnProperty("defaultValue")&&no(e,t.type,ht(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Hi(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function no(e,t,n){(t!=="number"||Or(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Sn=Array.isArray;function Zt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=ur.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function In(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Nn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},$c=["Webkit","ms","Moz","O"];Object.keys(Nn).forEach(function(e){$c.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Nn[t]=Nn[e]})});function ys(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Nn.hasOwnProperty(e)&&Nn[e]?(""+t).trim():t+"px"}function ks(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=ys(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Ac=H({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function oo(e,t){if(t){if(Ac[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(w(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(w(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(w(61))}if(t.style!=null&&typeof t.style!="object")throw Error(w(62))}}function io(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var uo=null;function qo(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var so=null,Jt=null,qt=null;function Yi(e){if(e=er(e)){if(typeof so!="function")throw Error(w(280));var t=e.stateNode;t&&(t=sl(t),so(e.stateNode,e.type,t))}}function ws(e){Jt?qt?qt.push(e):qt=[e]:Jt=e}function xs(){if(Jt){var e=Jt,t=qt;if(qt=Jt=null,Yi(e),t)for(e=0;e>>=0,e===0?32:31-(Jc(e)/qc|0)|0}var sr=64,ar=4194304;function Cn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Fr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=Cn(u):(o&=i,o!==0&&(r=Cn(o)))}else i=n&~l,i!==0?r=Cn(i):o!==0&&(r=Cn(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function qn(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Ie(t),e[t]=n}function nf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=_n),nu=" ",ru=!1;function Vs(e,t){switch(e){case"keyup":return Tf.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Bs(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var $t=!1;function Of(e,t){switch(e){case"compositionend":return Bs(t);case"keypress":return t.which!==32?null:(ru=!0,nu);case"textInput":return e=t.data,e===nu&&ru?null:e;default:return null}}function Mf(e,t){if($t)return e==="compositionend"||!ii&&Vs(e,t)?(e=$s(),jr=ri=lt=null,$t=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=uu(n)}}function Ks(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ks(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ys(){for(var e=window,t=Or();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Or(e.document)}return t}function ui(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Wf(e){var t=Ys(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Ks(n.ownerDocument.documentElement,n)){if(r!==null&&ui(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=su(n,o);var i=su(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,At=null,mo=null,zn=null,vo=!1;function au(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;vo||At==null||At!==Or(r)||(r=At,"selectionStart"in r&&ui(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),zn&&Vn(zn,r)||(zn=r,r=Ar(mo,"onSelect"),0Wt||(e.current=So[Wt],So[Wt]=null,Wt--)}function F(e,t){Wt++,So[Wt]=e.current,e.current=t}var mt={},ie=gt(mt),ve=gt(!1),zt=mt;function rn(e,t){var n=e.type.contextTypes;if(!n)return mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ge(e){return e=e.childContextTypes,e!=null}function Br(){$(ve),$(ie)}function vu(e,t,n){if(ie.current!==mt)throw Error(w(168));F(ie,t),F(ve,n)}function na(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(w(108,Fc(e)||"Unknown",l));return H({},n,r)}function Wr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||mt,zt=ie.current,F(ie,e),F(ve,ve.current),!0}function gu(e,t,n){var r=e.stateNode;if(!r)throw Error(w(169));n?(e=na(e,t,zt),r.__reactInternalMemoizedMergedChildContext=e,$(ve),$(ie),F(ie,e)):$(ve),F(ve,n)}var He=null,al=!1,$l=!1;function ra(e){He===null?He=[e]:He.push(e)}function td(e){al=!0,ra(e)}function yt(){if(!$l&&He!==null){$l=!0;var e=0,t=D;try{var n=He;for(D=1;e>=i,l-=i,Qe=1<<32-Ie(t)+l|n<z?(V=_,_=null):V=_.sibling;var C=m(d,_,p[z],g);if(C===null){_===null&&(_=V);break}e&&_&&C.alternate===null&&t(d,_),f=o(C,f,z),j===null?S=C:j.sibling=C,j=C,_=V}if(z===p.length)return n(d,_),A&&St(d,z),S;if(_===null){for(;zz?(V=_,_=null):V=_.sibling;var R=m(d,_,C.value,g);if(R===null){_===null&&(_=V);break}e&&_&&R.alternate===null&&t(d,_),f=o(R,f,z),j===null?S=R:j.sibling=R,j=R,_=V}if(C.done)return n(d,_),A&&St(d,z),S;if(_===null){for(;!C.done;z++,C=p.next())C=v(d,C.value,g),C!==null&&(f=o(C,f,z),j===null?S=C:j.sibling=C,j=C);return A&&St(d,z),S}for(_=r(d,_);!C.done;z++,C=p.next())C=k(_,d,z,C.value,g),C!==null&&(e&&C.alternate!==null&&_.delete(C.key===null?z:C.key),f=o(C,f,z),j===null?S=C:j.sibling=C,j=C);return e&&_.forEach(function(pe){return t(d,pe)}),A&&St(d,z),S}function P(d,f,p,g){if(typeof p=="object"&&p!==null&&p.type===Ut&&p.key===null&&(p=p.props.children),typeof p=="object"&&p!==null){switch(p.$$typeof){case or:e:{for(var S=p.key,j=f;j!==null;){if(j.key===S){if(S=p.type,S===Ut){if(j.tag===7){n(d,j.sibling),f=l(j,p.props.children),f.return=d,d=f;break e}}else if(j.elementType===S||typeof S=="object"&&S!==null&&S.$$typeof===et&&wu(S)===j.type){n(d,j.sibling),f=l(j,p.props),f.ref=yn(d,j,p),f.return=d,d=f;break e}n(d,j);break}else t(d,j);j=j.sibling}p.type===Ut?(f=Pt(p.props.children,d.mode,g,p.key),f.return=d,d=f):(g=Rr(p.type,p.key,p.props,null,d.mode,g),g.ref=yn(d,f,p),g.return=d,d=g)}return i(d);case Ft:e:{for(j=p.key;f!==null;){if(f.key===j)if(f.tag===4&&f.stateNode.containerInfo===p.containerInfo&&f.stateNode.implementation===p.implementation){n(d,f.sibling),f=l(f,p.children||[]),f.return=d,d=f;break e}else{n(d,f);break}else t(d,f);f=f.sibling}f=Yl(p,d.mode,g),f.return=d,d=f}return i(d);case et:return j=p._init,P(d,f,j(p._payload),g)}if(Sn(p))return y(d,f,p,g);if(pn(p))return x(d,f,p,g);vr(d,p)}return typeof p=="string"&&p!==""||typeof p=="number"?(p=""+p,f!==null&&f.tag===6?(n(d,f.sibling),f=l(f,p),f.return=d,d=f):(n(d,f),f=Kl(p,d.mode,g),f.return=d,d=f),i(d)):n(d,f)}return P}var on=ua(!0),sa=ua(!1),Kr=gt(null),Yr=null,Kt=null,fi=null;function di(){fi=Kt=Yr=null}function pi(e){var t=Kr.current;$(Kr),e._currentValue=t}function No(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function en(e,t){Yr=e,fi=Kt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(me=!0),e.firstContext=null)}function ze(e){var t=e._currentValue;if(fi!==e)if(e={context:e,memoizedValue:t,next:null},Kt===null){if(Yr===null)throw Error(w(308));Kt=e,Yr.dependencies={lanes:0,firstContext:e}}else Kt=Kt.next=e;return t}var Nt=null;function hi(e){Nt===null?Nt=[e]:Nt.push(e)}function aa(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,hi(t)):(n.next=l.next,l.next=n),t.interleaved=n,Ze(e,r)}function Ze(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var tt=!1;function mi(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function ca(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Ye(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function ct(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,I&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Ze(e,n)}return l=r.interleaved,l===null?(t.next=t,hi(r)):(t.next=l.next,l.next=t),r.interleaved=t,Ze(e,n)}function Er(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ei(e,n)}}function xu(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=i:o=o.next=i,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Gr(e,t,n,r){var l=e.updateQueue;tt=!1;var o=l.firstBaseUpdate,i=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var s=u,c=s.next;s.next=null,i===null?o=c:i.next=c,i=s;var h=e.alternate;h!==null&&(h=h.updateQueue,u=h.lastBaseUpdate,u!==i&&(u===null?h.firstBaseUpdate=c:u.next=c,h.lastBaseUpdate=s))}if(o!==null){var v=l.baseState;i=0,h=c=s=null,u=o;do{var m=u.lane,k=u.eventTime;if((r&m)===m){h!==null&&(h=h.next={eventTime:k,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var y=e,x=u;switch(m=t,k=n,x.tag){case 1:if(y=x.payload,typeof y=="function"){v=y.call(k,v,m);break e}v=y;break e;case 3:y.flags=y.flags&-65537|128;case 0:if(y=x.payload,m=typeof y=="function"?y.call(k,v,m):y,m==null)break e;v=H({},v,m);break e;case 2:tt=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,m=l.effects,m===null?l.effects=[u]:m.push(u))}else k={eventTime:k,lane:m,tag:u.tag,payload:u.payload,callback:u.callback,next:null},h===null?(c=h=k,s=v):h=h.next=k,i|=m;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;m=u,u=m.next,m.next=null,l.lastBaseUpdate=m,l.shared.pending=null}}while(!0);if(h===null&&(s=v),l.baseState=s,l.firstBaseUpdate=c,l.lastBaseUpdate=h,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);Rt|=i,e.lanes=i,e.memoizedState=v}}function Su(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Vl.transition;Vl.transition={};try{e(!1),t()}finally{D=n,Vl.transition=r}}function _a(){return Le().memoizedState}function od(e,t,n){var r=dt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Pa(e))za(t,n);else if(n=aa(e,t,n,r),n!==null){var l=se();De(n,e,r,l),La(n,t,r)}}function id(e,t,n){var r=dt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Pa(e))za(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,Fe(u,i)){var s=t.interleaved;s===null?(l.next=l,hi(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=aa(e,t,l,r),n!==null&&(l=se(),De(n,e,r,l),La(n,t,r))}}function Pa(e){var t=e.alternate;return e===W||t!==null&&t===W}function za(e,t){Ln=Zr=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function La(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ei(e,n)}}var Jr={readContext:ze,useCallback:re,useContext:re,useEffect:re,useImperativeHandle:re,useInsertionEffect:re,useLayoutEffect:re,useMemo:re,useReducer:re,useRef:re,useState:re,useDebugValue:re,useDeferredValue:re,useTransition:re,useMutableSource:re,useSyncExternalStore:re,useId:re,unstable_isNewReconciler:!1},ud={readContext:ze,useCallback:function(e,t){return $e().memoizedState=[e,t===void 0?null:t],e},useContext:ze,useEffect:ju,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Pr(4194308,4,Sa.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Pr(4194308,4,e,t)},useInsertionEffect:function(e,t){return Pr(4,2,e,t)},useMemo:function(e,t){var n=$e();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=$e();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=od.bind(null,W,e),[r.memoizedState,e]},useRef:function(e){var t=$e();return e={current:e},t.memoizedState=e},useState:Cu,useDebugValue:Ci,useDeferredValue:function(e){return $e().memoizedState=e},useTransition:function(){var e=Cu(!1),t=e[0];return e=ld.bind(null,e[1]),$e().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=W,l=$e();if(A){if(n===void 0)throw Error(w(407));n=n()}else{if(n=t(),b===null)throw Error(w(349));Tt&30||ha(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,ju(va.bind(null,r,o,e),[e]),r.flags|=2048,Xn(9,ma.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=$e(),t=b.identifierPrefix;if(A){var n=Ke,r=Qe;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Yn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Ae]=t,e[Hn]=r,Aa(e,t,!1,!1),t.stateNode=e;e:{switch(i=io(n,r),n){case"dialog":U("cancel",e),U("close",e),l=r;break;case"iframe":case"object":case"embed":U("load",e),l=r;break;case"video":case"audio":for(l=0;lan&&(t.flags|=128,r=!0,kn(o,!1),t.lanes=4194304)}else{if(!r)if(e=Xr(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),kn(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!A)return le(t),null}else 2*Y()-o.renderingStartTime>an&&n!==1073741824&&(t.flags|=128,r=!0,kn(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=Y(),t.sibling=null,n=B.current,F(B,r?n&1|2:n&1),t):(le(t),null);case 22:case 23:return zi(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ke&1073741824&&(le(t),t.subtreeFlags&6&&(t.flags|=8192)):le(t),null;case 24:return null;case 25:return null}throw Error(w(156,t.tag))}function md(e,t){switch(ai(t),t.tag){case 1:return ge(t.type)&&Br(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return un(),$(ve),$(ie),yi(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return gi(t),null;case 13:if($(B),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(w(340));ln()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return $(B),null;case 4:return un(),null;case 10:return pi(t.type._context),null;case 22:case 23:return zi(),null;case 24:return null;default:return null}}var yr=!1,oe=!1,vd=typeof WeakSet=="function"?WeakSet:Set,N=null;function Yt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Q(e,t,r)}else n.current=null}function Mo(e,t,n){try{n()}catch(r){Q(e,t,r)}}var Iu=!1;function gd(e,t){if(go=Ur,e=Ys(),ui(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,c=0,h=0,v=e,m=null;t:for(;;){for(var k;v!==n||l!==0&&v.nodeType!==3||(u=i+l),v!==o||r!==0&&v.nodeType!==3||(s=i+r),v.nodeType===3&&(i+=v.nodeValue.length),(k=v.firstChild)!==null;)m=v,v=k;for(;;){if(v===e)break t;if(m===n&&++c===l&&(u=i),m===o&&++h===r&&(s=i),(k=v.nextSibling)!==null)break;v=m,m=v.parentNode}v=k}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(yo={focusedElem:e,selectionRange:n},Ur=!1,N=t;N!==null;)if(t=N,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,N=e;else for(;N!==null;){t=N;try{var y=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(y!==null){var x=y.memoizedProps,P=y.memoizedState,d=t.stateNode,f=d.getSnapshotBeforeUpdate(t.elementType===t.type?x:Re(t.type,x),P);d.__reactInternalSnapshotBeforeUpdate=f}break;case 3:var p=t.stateNode.containerInfo;p.nodeType===1?p.textContent="":p.nodeType===9&&p.documentElement&&p.removeChild(p.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(w(163))}}catch(g){Q(t,t.return,g)}if(e=t.sibling,e!==null){e.return=t.return,N=e;break}N=t.return}return y=Iu,Iu=!1,y}function Tn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Mo(t,n,o)}l=l.next}while(l!==r)}}function dl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Io(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Wa(e){var t=e.alternate;t!==null&&(e.alternate=null,Wa(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ae],delete t[Hn],delete t[xo],delete t[bf],delete t[ed])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Ha(e){return e.tag===5||e.tag===3||e.tag===4}function Du(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ha(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Do(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Vr));else if(r!==4&&(e=e.child,e!==null))for(Do(e,t,n),e=e.sibling;e!==null;)Do(e,t,n),e=e.sibling}function Fo(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Fo(e,t,n),e=e.sibling;e!==null;)Fo(e,t,n),e=e.sibling}var ee=null,Oe=!1;function be(e,t,n){for(n=n.child;n!==null;)Qa(e,t,n),n=n.sibling}function Qa(e,t,n){if(Ve&&typeof Ve.onCommitFiberUnmount=="function")try{Ve.onCommitFiberUnmount(ll,n)}catch{}switch(n.tag){case 5:oe||Yt(n,t);case 6:var r=ee,l=Oe;ee=null,be(e,t,n),ee=r,Oe=l,ee!==null&&(Oe?(e=ee,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ee.removeChild(n.stateNode));break;case 18:ee!==null&&(Oe?(e=ee,n=n.stateNode,e.nodeType===8?Ul(e.parentNode,n):e.nodeType===1&&Ul(e,n),$n(e)):Ul(ee,n.stateNode));break;case 4:r=ee,l=Oe,ee=n.stateNode.containerInfo,Oe=!0,be(e,t,n),ee=r,Oe=l;break;case 0:case 11:case 14:case 15:if(!oe&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Mo(n,t,i),l=l.next}while(l!==r)}be(e,t,n);break;case 1:if(!oe&&(Yt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){Q(n,t,u)}be(e,t,n);break;case 21:be(e,t,n);break;case 22:n.mode&1?(oe=(r=oe)||n.memoizedState!==null,be(e,t,n),oe=r):be(e,t,n);break;default:be(e,t,n)}}function Fu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new vd),t.forEach(function(r){var l=Ed.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Te(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=Y()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*kd(r/1960))-r,10e?16:e,ot===null)var r=!1;else{if(e=ot,ot=null,el=0,I&6)throw Error(w(331));var l=I;for(I|=4,N=e.current;N!==null;){var o=N,i=o.child;if(N.flags&16){var u=o.deletions;if(u!==null){for(var s=0;sY()-_i?_t(e,0):Ei|=n),ye(e,t)}function ba(e,t){t===0&&(e.mode&1?(t=ar,ar<<=1,!(ar&130023424)&&(ar=4194304)):t=1);var n=se();e=Ze(e,t),e!==null&&(qn(e,t,n),ye(e,n))}function Nd(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),ba(e,n)}function Ed(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(w(314))}r!==null&&r.delete(t),ba(e,n)}var ec;ec=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ve.current)me=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return me=!1,pd(e,t,n);me=!!(e.flags&131072)}else me=!1,A&&t.flags&1048576&&la(t,Qr,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;zr(e,t),e=t.pendingProps;var l=rn(t,ie.current);en(t,n),l=wi(null,t,r,e,l,n);var o=xi();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ge(r)?(o=!0,Wr(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,mi(t),l.updater=fl,t.stateNode=l,l._reactInternals=t,_o(t,r,e,n),t=Lo(null,t,r,!0,o,n)):(t.tag=0,A&&o&&si(t),ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(zr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Pd(r),e=Re(r,e),l){case 0:t=zo(null,t,r,e,n);break e;case 1:t=Ru(null,t,r,e,n);break e;case 11:t=Lu(null,t,r,e,n);break e;case 14:t=Tu(null,t,r,Re(r.type,e),n);break e}throw Error(w(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),zo(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Ru(e,t,r,l,n);case 3:e:{if(Fa(t),e===null)throw Error(w(387));r=t.pendingProps,o=t.memoizedState,l=o.element,ca(e,t),Gr(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=sn(Error(w(423)),t),t=Ou(e,t,r,n,l);break e}else if(r!==l){l=sn(Error(w(424)),t),t=Ou(e,t,r,n,l);break e}else for(we=at(t.stateNode.containerInfo.firstChild),xe=t,A=!0,Me=null,n=sa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(ln(),r===l){t=Je(e,t,n);break e}ue(e,t,r,n)}t=t.child}return t;case 5:return fa(t),e===null&&jo(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,ko(r,l)?i=null:o!==null&&ko(r,o)&&(t.flags|=32),Da(e,t),ue(e,t,i,n),t.child;case 6:return e===null&&jo(t),null;case 13:return Ua(e,t,n);case 4:return vi(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=on(t,null,r,n):ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),Lu(e,t,r,l,n);case 7:return ue(e,t,t.pendingProps,n),t.child;case 8:return ue(e,t,t.pendingProps.children,n),t.child;case 12:return ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,F(Kr,r._currentValue),r._currentValue=i,o!==null)if(Fe(o.value,i)){if(o.children===l.children&&!ve.current){t=Je(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=Ye(-1,n&-n),s.tag=2;var c=o.updateQueue;if(c!==null){c=c.shared;var h=c.pending;h===null?s.next=s:(s.next=h.next,h.next=s),c.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),No(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(w(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),No(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,en(t,n),l=ze(l),r=r(l),t.flags|=1,ue(e,t,r,n),t.child;case 14:return r=t.type,l=Re(r,t.pendingProps),l=Re(r.type,l),Tu(e,t,r,l,n);case 15:return Ma(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Re(r,l),zr(e,t),t.tag=1,ge(r)?(e=!0,Wr(t)):e=!1,en(t,n),Ta(t,r,l),_o(t,r,l,n),Lo(null,t,r,!0,e,n);case 19:return $a(e,t,n);case 22:return Ia(e,t,n)}throw Error(w(156,t.tag))};function tc(e,t){return Ps(e,t)}function _d(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function _e(e,t,n,r){return new _d(e,t,n,r)}function Ti(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Pd(e){if(typeof e=="function")return Ti(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Zo)return 11;if(e===Jo)return 14}return 2}function pt(e,t){var n=e.alternate;return n===null?(n=_e(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Rr(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Ti(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case Ut:return Pt(n.children,l,o,t);case Xo:i=8,l|=8;break;case Zl:return e=_e(12,n,t,l|2),e.elementType=Zl,e.lanes=o,e;case Jl:return e=_e(13,n,t,l),e.elementType=Jl,e.lanes=o,e;case ql:return e=_e(19,n,t,l),e.elementType=ql,e.lanes=o,e;case fs:return hl(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case as:i=10;break e;case cs:i=9;break e;case Zo:i=11;break e;case Jo:i=14;break e;case et:i=16,r=null;break e}throw Error(w(130,e==null?e:typeof e,""))}return t=_e(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Pt(e,t,n,r){return e=_e(7,e,r,t),e.lanes=n,e}function hl(e,t,n,r){return e=_e(22,e,r,t),e.elementType=fs,e.lanes=n,e.stateNode={isHidden:!1},e}function Kl(e,t,n){return e=_e(6,e,null,t),e.lanes=n,e}function Yl(e,t,n){return t=_e(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function zd(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=_l(0),this.expirationTimes=_l(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=_l(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ri(e,t,n,r,l,o,i,u,s){return e=new zd(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=_e(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},mi(o),e}function Ld(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(oc)}catch(e){console.error(e)}}oc(),os.exports=Ce;var Id=os.exports,Qu=Id;Gl.createRoot=Qu.createRoot,Gl.hydrateRoot=Qu.hydrateRoot;const Xt=[{id:"anatomy",label:"Anatomy",ks:"cacheonly",name:"session"},{id:"kv-write",label:"KV write",ks:"cacheonly",name:"session"},{id:"loadthrough",label:"LoadThrough",ks:"loadthrough",name:"chart"},{id:"tombstone",label:"Tombstone",ks:"cacheonly",name:"session"},{id:"bloom",label:"Bloom",ks:"bloom",name:"users"},{id:"set",label:"Set",ks:"set",name:"flags"},{id:"zset",label:"ZSet",ks:"zset",name:"board"},{id:"geo",label:"Geo",ks:"geo",name:"places"},{id:"list",label:"List",ks:"list",name:"inbox"},{id:"hash",label:"Hash",ks:"hash",name:"profile"},{id:"counter",label:"Counter",ks:"counter",name:"rl"},{id:"json",label:"JSON",ks:"json",name:"doc"},{id:"bitmap",label:"Bitmap",ks:"bitmap",name:"seen"},{id:"hll",label:"HLL",ks:"hll",name:"uniques"},{id:"topk",label:"TopK",ks:"topk",name:"hot"},{id:"cms",label:"CMS",ks:"cms",name:"freq"},{id:"vectorset",label:"VectorSet",ks:"vectorset",name:"items"}];async function Dd(){const e=await fetch("/v1/cluster");if(!e.ok)throw new Error(await e.text());return e.json()}async function Ku(e){const t=await fetch("/v1/connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await t.json();if(!t.ok)throw new Error(n.error||t.statusText);return n}async function Fd(){const e=await fetch("/v1/disconnect",{method:"POST"});if(!e.ok)throw new Error(await e.text());return e.json()}async function Ud(e,t,n){const r=new URLSearchParams({ks:e,name:t,item:n}),l=await fetch(`/v1/bloom?${r}`);if(!l.ok)throw new Error(await l.text());return l.json()}async function $d(e,t){const n=await fetch(`/v1/view?ks=${encodeURIComponent(e)}&key=${encodeURIComponent(t)}`);if(!n.ok)throw new Error(await n.text());return n.json()}async function Ad(e,t,n,r={},l=""){return await(await fetch("/v1/op",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ks:e,op:t,name:n,via:l,args:r})})).json()}async function Vd(e){const t=await fetch(`/v1/scene/${encodeURIComponent(e)}`,{method:"POST"});if(!t.ok)throw new Error(await t.text());return t.json()}async function Bd(e=[]){await fetch("/v1/reset",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({names:e})})}function Wd(){const e=new URLSearchParams(window.location.search).get("chapter");return e&&Xt.some(t=>t.id===e)?e:"anatomy"}function Hd(e){const t=new URL(window.location.href);t.searchParams.set("chapter",e),window.history.replaceState(null,"",t.toString())}function Qd({current:e,onPick:t}){return a.jsx("nav",{className:"nav",children:Xt.map(n=>a.jsx("button",{className:n.id===e?"active":"",onClick:()=>t(n.id),children:n.label},n.id))})}function Kd({cluster:e,view:t,last:n,name:r,setName:l,onRefresh:o}){return a.jsxs("aside",{className:"inspector",children:[a.jsx("h2",{children:"Results"}),a.jsxs("dl",{children:[a.jsx("dt",{children:"name"}),a.jsx("dd",{children:a.jsx("input",{value:r,onChange:i=>l(i.target.value),onBlur:o,placeholder:"key or name, e.g. session"})}),a.jsx("dt",{children:"owner"}),a.jsx("dd",{children:a.jsx("code",{children:(t==null?void 0:t.owner)||"—"})}),a.jsx("dt",{children:"RF"}),a.jsx("dd",{children:(t==null?void 0:t.rf)??(e==null?void 0:e.rf)??"—"}),a.jsx("dt",{children:"ring gen"}),a.jsx("dd",{children:(t==null?void 0:t.ring_gen)??(e==null?void 0:e.ring_gen)??"—"}),a.jsx("dt",{children:"SoT loads"}),a.jsxs("dd",{children:[(e==null?void 0:e.sot_loads)??0," ",a.jsxs("span",{className:"muted",children:["(",e==null?void 0:e.sot_latency,")"]})]})]}),(n==null?void 0:n.trace)&&n.trace.length>0&&a.jsxs(a.Fragment,{children:[a.jsx("h2",{children:"Trace"}),a.jsx("div",{className:"trace",children:n.trace.map(i=>a.jsx("div",{children:i},i))})]}),n&&a.jsxs(a.Fragment,{children:[a.jsx("h2",{children:"Last result"}),n.error&&a.jsx("p",{children:n.invalid_argument?"invalid argument":n.error}),a.jsx("pre",{className:"result",children:JSON.stringify(n.result??n,null,2)})]})]})}function fe({when:e,not:t}){return a.jsxs("p",{className:"note",children:[a.jsx("strong",{children:"Use when"})," ",e," · ",a.jsx("strong",{children:"Not"})," ",t]})}function de({name:e,setName:t,placeholder:n="structure name, e.g. session"}){return a.jsxs("label",{className:"stack",children:["name",a.jsx("input",{value:e,onChange:r=>t(r.target.value),placeholder:n})]})}function Yd({name:e,setName:t,run:n,last:r}){const[l,o]=L.useState(Array(64).fill(!1)),[i,u]=L.useState(0),s=r==null?void 0:r.result;async function c(k,y){(await n("bitset",{offset:k,bit:y})).ok&&(u(k),kP.map((d,f)=>f===k?y:d)))}async function h(k){await c(k,!l[k])}async function v(k=i){u(k),await n("bitget",{offset:k})}async function m(){await n("delete"),o(Array(64).fill(!1))}return a.jsxs("div",{children:[a.jsx(fe,{when:"packed flags / presence bits.",not:"a Bloom filter — this is exact and MSB-first."}),a.jsx(de,{name:e,setName:t,placeholder:"bitmap name, e.g. seen"}),a.jsxs("label",{className:"stack",children:["offset",a.jsx("input",{type:"number",min:0,value:i,onChange:k=>u(Math.max(0,Number(k.target.value)||0)),placeholder:"bit index from 0, e.g. 0"})]}),a.jsxs("div",{className:"stack-actions",children:[a.jsx("button",{type:"button",onClick:()=>void c(i,!0),children:"Set bit 1"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void c(i,!1),children:"Set bit 0"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void v(),children:"Get bit"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void n("bitcount",{start:0,end:-1}),children:"Count ones"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void m(),children:"Delete bitmap"})]}),(s==null?void 0:s.count)!==void 0&&a.jsxs("p",{className:"bloom-hit",children:["count = ",s.count]}),(s==null?void 0:s.present)!==void 0&&s.bit!==void 0&&a.jsxs("p",{className:s.bit?"bloom-hit":"bloom-miss",children:["offset ",i," → ",s.present?s.bit?"1":"0":"missing"]}),a.jsx("p",{className:"note",children:"Click a cell to toggle that offset (0–63)."}),a.jsx("div",{className:"bits",children:l.map((k,y)=>a.jsx("button",{type:"button",className:k?"bit on":"bit",onClick:()=>void h(y),title:`offset ${y}`,children:y},y))})]})}function Gd({name:e,setName:t,run:n,last:r}){const[l,o]=L.useState("alice"),[i,u]=L.useState(null),s=r==null?void 0:r.result,c=r&&"maybe"in(r.result??{});L.useEffect(()=>{let P=!1;return Ud("bloom",e,l).then(d=>{P||u(d)}).catch(()=>{P||u(null)}),()=>{P=!0}},[e,l,r]);async function h(P=l){const d=P.trim();d&&(o(d),await n("bloomadd",{item:d}))}async function v(P=l){const d=P.trim();d&&(o(d),await n("bloomtest",{item:d}))}async function m(){await n("delete")}const k=(i==null?void 0:i.bits)??Array(64).fill(!1),y=new Set((i==null?void 0:i.positions)??[]),x=c?s==null?void 0:s.maybe:i==null?void 0:i.maybe;return a.jsxs("div",{children:[a.jsx(fe,{when:"cheap maybe-membership.",not:"exact — this is not a bitmap you toggle."}),a.jsx(de,{name:e,setName:t,placeholder:"filter name, e.g. users"}),a.jsxs("label",{className:"stack",children:["item",a.jsx("input",{value:l,onChange:P=>o(P.target.value),onKeyDown:P=>{P.key==="Enter"&&h()},placeholder:"member to add/test, e.g. alice"})]}),a.jsxs("div",{className:"stack-actions",children:[a.jsx("button",{type:"button",onClick:()=>void h(),children:"Add to filter"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void v(),children:"Test membership"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void m(),children:"Delete filter"})]}),x!==void 0&&l.trim()&&a.jsxs("p",{className:x?"bloom-hit":"bloom-miss",children:[l," → ",x?"maybe":"no",i&&` · k=${i.k} hashes`]}),a.jsx("p",{className:"note",children:"Grid is the 64-bit filter (same idea as Bitmap). Yellow ring = hash slots for the item in the box."}),a.jsx("div",{className:"bits",children:k.map((P,d)=>a.jsx("span",{className:"bit"+(P?" on":"")+(y.has(d)?" probe":""),title:`bit ${d}${y.has(d)?" (hash)":""}${P?" set":""}`,children:d},d))})]})}function Xd({name:e,setName:t,run:n,last:r}){var c;const[l,o]=L.useState("t003"),[i,u]=L.useState(1),s=(c=r==null?void 0:r.result)==null?void 0:c.count;return a.jsxs("div",{children:[a.jsx(fe,{when:"approximate frequency of any item.",not:"TopK — CMS still answers after eviction."}),a.jsx(de,{name:e,setName:t,placeholder:"sketch name, e.g. freq"}),a.jsxs("label",{className:"stack",children:["item",a.jsx("input",{value:l,onChange:h=>o(h.target.value),placeholder:"item to count, e.g. t003"})]}),a.jsxs("label",{className:"stack",children:["n (0 means 1)",a.jsx("input",{type:"number",value:i,onChange:h=>u(Number(h.target.value)),placeholder:"increment count, e.g. 1"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("cmsincr",{item:l,n:i}),children:"Incr"}),a.jsx("button",{className:"ghost",onClick:()=>n("cmsquery",{item:l}),children:"Query"}),s!==void 0&&a.jsx("strong",{children:s})]})]})}function Zd({name:e,setName:t,run:n}){const[r,l]=L.useState("hello");return a.jsxs("div",{children:[a.jsx(fe,{when:"opaque KV with no backend.",not:"a source of truth — writes ACK on the owner."}),a.jsx(de,{name:e,setName:t,placeholder:"key, e.g. session"}),a.jsxs("label",{className:"stack",children:["value (bytes as text)",a.jsx("input",{value:r,onChange:o=>l(o.target.value),placeholder:"opaque string, e.g. hello"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("put",{value:r}),children:"Put"}),a.jsx("button",{className:"ghost",onClick:()=>n("get"),children:"Get"}),a.jsx("button",{className:"ghost",onClick:()=>n("delete"),children:"Delete"})]})]})}function Jd({name:e,setName:t,run:n,last:r}){var u;const[l,o]=L.useState(1),i=(u=r==null?void 0:r.result)==null?void 0:u.value;return a.jsxs("div",{children:[a.jsx(fe,{when:"a single int64 (rate-limit windows, tallies).",not:"a float score — that is ZSet."}),a.jsx(de,{name:e,setName:t,placeholder:"counter name, e.g. rl"}),a.jsxs("label",{className:"stack",children:["delta",a.jsx("input",{type:"number",value:l,onChange:s=>o(Number(s.target.value)),placeholder:"int64 to add, e.g. 1 or -1"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("incr",{delta:l}),children:"Incr"}),a.jsx("button",{className:"ghost",onClick:()=>n("cget"),children:"Get"}),a.jsx("button",{className:"ghost",onClick:()=>n("delete"),children:"Delete"}),i!==void 0&&a.jsx("strong",{style:{fontSize:"1.6rem"},children:i})]})]})}const Yu=["#38bdf8","#34d399","#fbbf24","#f472b6","#a78bfa","#fb7185","#2dd4bf","#f97316"];function qd(e){let t=0;for(let n=0;n>>0;return Yu[t%Yu.length]}function bd({name:e,setName:t,run:n,last:r}){var p;const[l,o]=L.useState("shop"),[i,u]=L.useState(-74),[s,c]=L.useState(40.7),[h,v]=L.useState(20),[m,k]=L.useState(10),y=((p=r==null?void 0:r.result)==null?void 0:p.members)??[],x=Math.max(h,.001),P=.35;function d(){return Math.max(0,h)*1e3}function f(g,S){const j=(S-s)*111.32;return{x:(g-i)*111.32*Math.cos(s*Math.PI/180)/x*P,y:-j/x*P}}return a.jsxs("div",{children:[a.jsx(fe,{when:"lon/lat radius queries.",not:"an embedding space — use VectorSet."}),a.jsx(de,{name:e,setName:t,placeholder:"index name, e.g. places"}),a.jsxs("label",{className:"stack",children:["member",a.jsx("input",{value:l,onChange:g=>o(g.target.value),placeholder:"point id, e.g. shop"})]}),a.jsxs("div",{className:"pair",children:[a.jsxs("label",{className:"stack",children:["lat",a.jsx("input",{type:"number",step:"0.01",value:s,onChange:g=>c(Number(g.target.value)),placeholder:"e.g. 40.7"})]}),a.jsxs("label",{className:"stack",children:["lng",a.jsx("input",{type:"number",step:"0.01",value:i,onChange:g=>u(Number(g.target.value)),placeholder:"e.g. -74.0"})]})]}),a.jsxs("label",{className:"stack",children:["radius (km)",a.jsx("input",{type:"number",min:0,step:"0.1",value:h,onChange:g=>v(Number(g.target.value)),placeholder:"search radius in km, e.g. 20"})]}),a.jsxs("label",{className:"stack",children:["limit (0 = all)",a.jsx("input",{type:"number",min:0,value:m,onChange:g=>k(Number(g.target.value)),placeholder:"max hits, e.g. 10"})]}),a.jsxs("div",{className:"stack-actions",children:[a.jsx("button",{type:"button",onClick:()=>void n("geoadd",{member:l,lon:i,lat:s}),children:"Add point"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void n("georadius",{lon:i,lat:s,radius:d(),limit:m}),children:"Radius query"}),a.jsx("button",{type:"button",className:"ghost",onClick:()=>void n("georem",{member:l}),children:"Remove member"})]}),y.length>0&&a.jsxs("p",{className:"note",children:[y.length," hit",y.length===1?"":"s"," within ",h," km"]}),a.jsxs("svg",{className:"plot",viewBox:"-1 -1 2 2",children:[a.jsx("circle",{cx:"0",cy:"0",r:P,fill:"none",stroke:"#334155"}),y.map(g=>{const S=f(g.lon,g.lat);return a.jsx("circle",{cx:S.x,cy:S.y,r:"0.028",fill:qd(g.member),fillOpacity:.5},g.member)})]})]})}function ep({name:e,setName:t,run:n,last:r}){var c;const[l,o]=L.useState("user-1"),[i,u]=L.useState(new Set),s=(c=r==null?void 0:r.result)==null?void 0:c.count;return a.jsxs("div",{children:[a.jsx(fe,{when:"approximate distinct count.",not:"an exact set — items are hashed away."}),a.jsx(de,{name:e,setName:t,placeholder:"sketch name, e.g. uniques"}),a.jsxs("label",{className:"stack",children:["item",a.jsx("input",{value:l,onChange:h=>o(h.target.value),placeholder:"hashed item, e.g. user-1"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:async()=>{await n("hlladd",{item:l}),u(h=>new Set(h).add(l))},children:"Add"}),a.jsx("button",{className:"ghost",onClick:()=>n("hllcount"),children:"Count"})]}),a.jsxs("p",{className:"note",children:["estimate ",s??"—"," · exact UI set ",i.size]})]})}function tp({name:e,setName:t,run:n,last:r}){var c;const[l,o]=L.useState("email"),[i,u]=L.useState("a@b"),s=((c=r==null?void 0:r.result)==null?void 0:c.fields)??[];return a.jsxs("div",{children:[a.jsx(fe,{when:"many fields that change independently.",not:"one JSON Put — that is a single LWW blob."}),a.jsx(de,{name:e,setName:t,placeholder:"hash name, e.g. profile"}),a.jsxs("label",{className:"stack",children:["field",a.jsx("input",{value:l,onChange:h=>o(h.target.value),placeholder:"field name, e.g. email"})]}),a.jsxs("label",{className:"stack",children:["value",a.jsx("input",{value:i,onChange:h=>u(h.target.value),placeholder:"field value, e.g. a@b"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("hset",{field:l,value:i}),children:"HSet"}),a.jsx("button",{className:"ghost",onClick:()=>n("hget",{field:l}),children:"HGet"}),a.jsx("button",{className:"ghost",onClick:()=>n("hdel",{field:l}),children:"HDel"}),a.jsx("button",{className:"ghost",onClick:()=>n("hgetall"),children:"HGetAll"})]}),s.map(h=>a.jsxs("div",{className:"note",children:[h.field," = ",h.value]},h.field))]})}function np({name:e,setName:t,run:n,last:r}){var c;const[l,o]=L.useState("$.name"),[i,u]=L.useState('"Ada"'),s=(c=r==null?void 0:r.result)==null?void 0:c.raw;return a.jsxs("div",{children:[a.jsx(fe,{when:"a nested document with path updates.",not:"per-field concurrency — use Hash."}),a.jsx(de,{name:e,setName:t,placeholder:"document name, e.g. doc"}),a.jsxs("label",{className:"stack",children:["path",a.jsx("input",{value:l,onChange:h=>o(h.target.value),placeholder:"JSON path, e.g. $.name or $"})]}),a.jsxs("label",{className:"stack",children:["JSON value",a.jsx("input",{value:i,onChange:h=>u(h.target.value),placeholder:'raw JSON, e.g. "Ada" or {"ok":true}'})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("jsonset",{path:l,value:i}),children:"Set"}),a.jsx("button",{className:"ghost",onClick:()=>n("jsonget",{path:"$"}),children:"Get $"}),a.jsx("button",{className:"ghost",onClick:()=>n("jsondel",{path:l}),children:"Del path"})]}),s&&a.jsx("pre",{className:"result",children:s})]})}function rp({name:e,setName:t,run:n,last:r}){var u;const[l,o]=L.useState("event1"),i=((u=r==null?void 0:r.result)==null?void 0:u.items)??[];return a.jsxs("div",{children:[a.jsx(fe,{when:"a queue or timeline.",not:"a set — order and duplicates matter."}),a.jsx(de,{name:e,setName:t,placeholder:"list name, e.g. inbox"}),a.jsxs("label",{className:"stack",children:["item",a.jsx("input",{value:l,onChange:s=>o(s.target.value),placeholder:"list element, e.g. event1"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("lpush",{item:l}),children:"LPush"}),a.jsx("button",{onClick:()=>n("rpush",{item:l}),children:"RPush"}),a.jsx("button",{className:"ghost",onClick:()=>n("lpop"),children:"LPop"}),a.jsx("button",{className:"ghost",onClick:()=>n("rpop"),children:"RPop"}),a.jsx("button",{className:"ghost",onClick:()=>n("lrange",{start:0,stop:-1}),children:"LRange"})]}),a.jsx("div",{className:"row",children:i.map((s,c)=>a.jsx("code",{children:s},c))})]})}function lp({name:e,setName:t,run:n,cluster:r}){return a.jsxs("div",{children:[a.jsx(fe,{when:"cache a backend (SoT) on miss, with singleflight.",not:"CacheOnly — a miss here loads."}),a.jsx(de,{name:e,setName:t,placeholder:"key to load, e.g. chart"}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("get"),children:"Get (may load SoT)"}),a.jsx("button",{className:"ghost",onClick:()=>n("delete"),children:"Delete / invalidate"}),a.jsxs("span",{className:"note",children:["SoT loads: ",(r==null?void 0:r.sot_loads)??0]})]})]})}function op({name:e,setName:t,run:n,last:r}){var u;const[l,o]=L.useState("dark_mode"),i=((u=r==null?void 0:r.result)==null?void 0:u.members)??[];return a.jsxs("div",{children:[a.jsx(fe,{when:"exact membership.",not:"a Bloom filter, and not Get/Put."}),a.jsx(de,{name:e,setName:t,placeholder:"set name, e.g. flags"}),a.jsxs("label",{className:"stack",children:["item",a.jsx("input",{value:l,onChange:s=>o(s.target.value),placeholder:"exact member, e.g. dark_mode"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("sadd",{item:l}),children:"Add"}),a.jsx("button",{className:"ghost",onClick:()=>n("srem",{item:l}),children:"Remove"}),a.jsx("button",{className:"ghost",onClick:()=>n("sismember",{item:l}),children:"Contains"}),a.jsx("button",{className:"ghost",onClick:()=>n("smembers"),children:"Members"}),a.jsx("button",{className:"ghost",onClick:()=>n("get"),children:"Wrong verb (Get)"})]}),i.length>0&&a.jsx("p",{className:"note",children:i.join(", ")})]})}function ip({name:e,setName:t,run:n,last:r}){var s;const[l,o]=L.useState("t001"),i=((s=r==null?void 0:r.result)==null?void 0:s.entries)??[],u=Math.max(1,...i.map(c=>c.count));return a.jsxs("div",{children:[a.jsx(fe,{when:"heavy hitters from a stream.",not:"ZAdd — you do not write the score."}),a.jsx(de,{name:e,setName:t,placeholder:"table name, e.g. hot"}),a.jsxs("label",{className:"stack",children:["item",a.jsx("input",{value:l,onChange:c=>o(c.target.value),placeholder:"observation id, e.g. t001"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("topkadd",{item:l}),children:"Observe"}),a.jsx("button",{className:"ghost",onClick:()=>n("topklist"),children:"List"})]}),a.jsx("div",{className:"bars",children:i.map(c=>a.jsx("div",{className:"bar",style:{height:`${c.count/u*100}%`},title:`${c.item} ${c.count}`},c.item))})]})}function up({name:e,setName:t,run:n,last:r}){var k;const[l,o]=L.useState("east"),[i,u]=L.useState(1),[s,c]=L.useState(0),[h,v]=L.useState([]),m=((k=r==null?void 0:r.result)==null?void 0:k.hits)??[];return a.jsxs("div",{children:[a.jsx(fe,{when:"small in-memory K-NN.",not:"HNSW / Faiss — brute force under the store mutex."}),a.jsx(de,{name:e,setName:t,placeholder:"set name, e.g. items"}),a.jsxs("label",{className:"stack",children:["member",a.jsx("input",{value:l,onChange:y=>o(y.target.value),placeholder:"vector id, e.g. east"})]}),a.jsxs("label",{className:"stack",children:["x (dim 0)",a.jsx("input",{type:"number",step:"0.1",value:i,onChange:y=>u(Number(y.target.value)),placeholder:"float32, e.g. 1"})]}),a.jsxs("label",{className:"stack",children:["y (dim 1)",a.jsx("input",{type:"number",step:"0.1",value:s,onChange:y=>c(Number(y.target.value)),placeholder:"float32, e.g. 0"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:async()=>{await n("vadd",{member:l,vec:[i,s]}),v(y=>[...y.filter(x=>x.member!==l),{member:l,x:i,y:s}])},children:"VAdd"}),a.jsx("button",{className:"ghost",onClick:()=>n("vsim",{vec:[i,s],k:3}),children:"VSim"}),a.jsx("button",{className:"ghost",onClick:()=>n("vrem",{member:l}),children:"VRem"})]}),a.jsxs("svg",{className:"plot",viewBox:"-1.2 -1.2 2.4 2.4",children:[a.jsx("line",{x1:"-1.2",y1:"0",x2:"1.2",y2:"0",stroke:"#334155"}),a.jsx("line",{x1:"0",y1:"-1.2",x2:"0",y2:"1.2",stroke:"#334155"}),h.map(y=>a.jsx("circle",{cx:y.x,cy:-y.y,r:"0.07",fill:"#38bdf8"},y.member)),a.jsx("line",{x1:"0",y1:"0",x2:i,y2:-s,stroke:"#fbbf24",strokeWidth:"0.03"})]}),m.length>0&&a.jsx("p",{className:"note",children:m.map(y=>`${y.member} ${y.score.toFixed(3)}`).join(" · ")})]})}function sp({name:e,setName:t,run:n,last:r}){var c;const[l,o]=L.useState("alice"),[i,u]=L.useState(100),s=((c=r==null?void 0:r.result)==null?void 0:c.members)??[];return a.jsxs("div",{children:[a.jsx(fe,{when:"an exact scored ranking you write.",not:"TopK — those are observations, not scores."}),a.jsx(de,{name:e,setName:t,placeholder:"zset name, e.g. board"}),a.jsxs("label",{className:"stack",children:["member",a.jsx("input",{value:l,onChange:h=>o(h.target.value),placeholder:"member id, e.g. alice"})]}),a.jsxs("label",{className:"stack",children:["score",a.jsx("input",{type:"number",value:i,onChange:h=>u(Number(h.target.value)),placeholder:"float score, e.g. 100"})]}),a.jsxs("div",{className:"row",children:[a.jsx("button",{onClick:()=>n("zadd",{member:l,score:i}),children:"ZAdd"}),a.jsx("button",{className:"ghost",onClick:()=>n("zrem",{member:l}),children:"ZRem"}),a.jsx("button",{className:"ghost",onClick:()=>n("zrange",{start:0,stop:-1}),children:"ZRange"})]}),s.length>0&&a.jsx("table",{children:a.jsx("tbody",{children:s.map(h=>a.jsxs("tr",{children:[a.jsx("td",{children:h.member}),a.jsx("td",{children:h.score})]},h.member))})})]})}function ap(e){var n;const t={name:e.name,setName:e.setName,via:e.via,run:e.run,last:e.last,cluster:e.cluster};return a.jsxs("section",{className:"play",children:[a.jsx("div",{className:"section-label",children:"Controls"}),a.jsxs("div",{className:"row",children:[a.jsx("h2",{children:e.chapter.label}),a.jsxs("label",{children:["ingress",a.jsx("select",{value:e.via,onChange:r=>e.setVia(r.target.value),children:(((n=e.cluster)==null?void 0:n.nodes)??[]).map(r=>a.jsx("option",{value:r.id,children:r.id},r.id))})]}),a.jsx("button",{onClick:e.onScene,children:"Run scene"})]}),a.jsx(cp,{id:e.chapter.id,w:t})]})}function cp({id:e,w:t}){switch(e){case"loadthrough":return a.jsx(lp,{...t});case"bloom":return a.jsx(Gd,{...t});case"set":return a.jsx(op,{...t});case"zset":return a.jsx(sp,{...t});case"geo":return a.jsx(bd,{...t});case"list":return a.jsx(rp,{...t});case"hash":return a.jsx(tp,{...t});case"counter":return a.jsx(Jd,{...t});case"json":return a.jsx(np,{...t});case"bitmap":return a.jsx(Yd,{...t});case"hll":return a.jsx(ep,{...t});case"topk":return a.jsx(ip,{...t});case"cms":return a.jsx(Xd,{...t});case"vectorset":return a.jsx(up,{...t});default:return a.jsx(Zd,{...t})}}function fp({view:e,last:t}){const n=(e==null?void 0:e.nodes)??[];return a.jsxs("section",{className:"canvas",children:[a.jsxs("div",{className:"nodes",children:[n.map(r=>{const l=["node",r.role,r.kind].join(" ");return a.jsxs("article",{className:l,children:[a.jsx("div",{className:"id",children:r.id}),a.jsx("div",{className:"meta",children:r.cache}),a.jsxs("div",{className:"meta",children:[r.role," · v",r.version," · ",r.bytes,"B"]}),a.jsx("span",{className:"kind "+r.kind,children:r.kind})]},r.id)}),n.length===0&&a.jsx("p",{className:"muted",children:"No backend. Connect cache gRPC addresses above, or start a local 3-node mesh."})]}),a.jsx("div",{className:"trace",children:((t==null?void 0:t.trace)??["Run a chapter or a verb — the mesh observation lands here."]).map(r=>a.jsx("div",{children:r},r))})]})}function dp(){const[e,t]=L.useState(Wd),n=L.useMemo(()=>Xt.find(C=>C.id===e)??Xt[0],[e]),[r,l]=L.useState(null),[o,i]=L.useState(null),[u,s]=L.useState(null),[c,h]=L.useState(""),[v,m]=L.useState(n.name),[k,y]=L.useState(!1),[x,P]=L.useState(!1),[d,f]=L.useState(()=>localStorage.getItem("lab.addrs")||"127.0.0.1:9000"),[p,g]=L.useState(""),S=L.useCallback(async()=>{var R;const C=await Dd();return l(C),C.nodes[0]&&!C.nodes.some(pe=>pe.id===c)&&h(C.nodes[0].id),(R=C.addrs)!=null&&R.length&&f(C.addrs.join(",")),C},[c]),j=L.useCallback(async(C=n.ks,R=v)=>{!C||!R||i(await $d(C,R))},[n.ks,v]);L.useEffect(()=>{S().then(()=>j())},[S,j]);function _(C){const R=Xt.find(pe=>pe.id===C)??Xt[0];t(R.id),Hd(R.id),m(R.name),s(null),j(R.ks,R.name)}async function z(C,R={}){P(!0);try{k&&await new Promise(kt=>setTimeout(kt,400));const pe=await Ad(n.ks,C,v,R,c);return s(pe),pe.after?i(pe.after):await j(),await S(),pe}finally{P(!1)}}async function V(){P(!0);try{const C=await Vd(n.id),R=C.steps[C.steps.length-1];R!=null&&R.resp&&(s(R.resp),R.resp.after&&i(R.resp.after)),await S()}finally{P(!1)}}return a.jsxs("div",{className:"shell",children:[a.jsxs("header",{className:"top",children:[a.jsx("h1",{children:"SuperCache Lab"}),a.jsxs("span",{className:"muted",children:[(r==null?void 0:r.mode)??"disconnected",r!=null&&r.connected?` · ${r.nodes.length} node(s)`:""]}),a.jsx("input",{className:"top-addrs",value:d,onChange:C=>f(C.target.value),placeholder:"cache gRPC, e.g. 127.0.0.1:9000,127.0.0.1:9010",title:p||"Comma-separated cache gRPC addresses"}),a.jsx("button",{type:"button",disabled:x,onClick:()=>{P(!0),g(""),localStorage.setItem("lab.addrs",d),Ku({addrs:d}).then(C=>{var R;return l(C),h(((R=C.nodes[0])==null?void 0:R.id)??""),s(null),j()}).catch(C=>g(C.message)).finally(()=>P(!1))},children:"Connect"}),a.jsx("button",{type:"button",className:"ghost",disabled:x,onClick:()=>{P(!0),g(""),Ku({in_process:!0}).then(C=>{var R;return l(C),h(((R=C.nodes[0])==null?void 0:R.id)??""),s(null),j()}).catch(C=>g(C.message)).finally(()=>P(!1))},children:"Local 3-node"}),a.jsx("button",{type:"button",className:"ghost",disabled:x||!(r!=null&&r.connected),onClick:()=>{P(!0),g(""),Fd().then(C=>{l(C),h(""),i(null),s(null)}).catch(C=>g(C.message)).finally(()=>P(!1))},children:"Disconnect"}),p&&a.jsx("span",{className:"bloom-miss",children:p}),a.jsx("span",{className:"grow"}),a.jsxs("label",{className:"muted",children:[a.jsx("input",{type:"checkbox",checked:k,onChange:C=>y(C.target.checked)})," slow-mo"]}),a.jsx("button",{className:"ghost",disabled:x,onClick:()=>{Bd().then(()=>{s(null),j(),S()})},children:"Reset"})]}),a.jsx(Qd,{current:n.id,onPick:_}),a.jsx(fp,{view:o,last:u}),a.jsxs("aside",{className:"sidebar",children:[a.jsx(ap,{chapter:n,via:c,setVia:h,cluster:r,name:v,setName:m,run:z,last:u,onScene:()=>void V()}),a.jsx(Kd,{cluster:r,view:o,last:u,name:v,setName:m,onRefresh:()=>void j()})]})]})}Gl.createRoot(document.getElementById("root")).render(a.jsx(Cc.StrictMode,{children:a.jsx(dp,{})})); diff --git a/examples/lab/ui/dist/index.html b/examples/lab/ui/dist/index.html new file mode 100644 index 0000000..ebf8351 --- /dev/null +++ b/examples/lab/ui/dist/index.html @@ -0,0 +1,13 @@ + + + + + + SuperCache Lab + + + + +
+ + diff --git a/examples/lab/ui/index.html b/examples/lab/ui/index.html new file mode 100644 index 0000000..245ca4a --- /dev/null +++ b/examples/lab/ui/index.html @@ -0,0 +1,12 @@ + + + + + + SuperCache Lab + + +
+ + + diff --git a/examples/lab/ui/package-lock.json b/examples/lab/ui/package-lock.json new file mode 100644 index 0000000..13a5a3d --- /dev/null +++ b/examples/lab/ui/package-lock.json @@ -0,0 +1,1755 @@ +{ + "name": "supercache-lab-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supercache-lab-ui", + "version": "0.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/examples/lab/ui/package.json b/examples/lab/ui/package.json new file mode 100644 index 0000000..a770aa7 --- /dev/null +++ b/examples/lab/ui/package.json @@ -0,0 +1,22 @@ +{ + "name": "supercache-lab-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/examples/lab/ui/src/App.tsx b/examples/lab/ui/src/App.tsx new file mode 100644 index 0000000..683160f --- /dev/null +++ b/examples/lab/ui/src/App.tsx @@ -0,0 +1,210 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { ChapterNav } from "./ChapterNav"; +import { Inspector } from "./Inspector"; +import { Playground } from "./Playground"; +import { + CHAPTERS, + chapterFromURL, + connectBackend, + disconnectBackend, + getCluster, + getView, + resetLab, + runOp, + runScene, + setChapterURL, + type ClusterInfo, + type OpResp, + type View, +} from "./api"; +import { ClusterCanvas } from "./cluster/Canvas"; + +export function App() { + const [chapterId, setChapterId] = useState(chapterFromURL); + const chapter = useMemo(() => CHAPTERS.find((c) => c.id === chapterId) ?? CHAPTERS[0], [chapterId]); + const [cluster, setCluster] = useState(null); + const [view, setView] = useState(null); + const [last, setLast] = useState(null); + const [via, setVia] = useState(""); + const [name, setName] = useState(chapter.name); + const [slowMo, setSlowMo] = useState(false); + const [busy, setBusy] = useState(false); + const [addrs, setAddrs] = useState(() => localStorage.getItem("lab.addrs") || "127.0.0.1:9000"); + const [backendErr, setBackendErr] = useState(""); + + const refreshCluster = useCallback(async () => { + const c = await getCluster(); + setCluster(c); + if (c.nodes[0] && !c.nodes.some((n) => n.id === via)) setVia(c.nodes[0].id); + if (c.addrs?.length) setAddrs(c.addrs.join(",")); + return c; + }, [via]); + + const refreshView = useCallback(async (ks = chapter.ks, key = name) => { + if (!ks || !key) return; + setView(await getView(ks, key)); + }, [chapter.ks, name]); + + useEffect(() => { + void refreshCluster().then(() => refreshView()); + }, [refreshCluster, refreshView]); + + function pickChapter(id: string) { + const ch = CHAPTERS.find((c) => c.id === id) ?? CHAPTERS[0]; + setChapterId(ch.id); + setChapterURL(ch.id); + setName(ch.name); + setLast(null); + void refreshView(ch.ks, ch.name); + } + + async function doOp(op: string, args: Record = {}) { + setBusy(true); + try { + if (slowMo) await new Promise((r) => setTimeout(r, 400)); + const resp = await runOp(chapter.ks, op, name, args, via); + setLast(resp); + if (resp.after) setView(resp.after); + else await refreshView(); + await refreshCluster(); + return resp; + } finally { + setBusy(false); + } + } + + async function doScene() { + setBusy(true); + try { + const scene = await runScene(chapter.id); + const lastStep = scene.steps[scene.steps.length - 1]; + if (lastStep?.resp) { + setLast(lastStep.resp); + if (lastStep.resp.after) setView(lastStep.resp.after); + } + await refreshCluster(); + } finally { + setBusy(false); + } + } + + return ( +
+
+

SuperCache Lab

+ + {cluster?.mode ?? "disconnected"} + {cluster?.connected ? ` · ${cluster.nodes.length} node(s)` : ""} + + setAddrs(e.target.value)} + placeholder="cache gRPC, e.g. 127.0.0.1:9000,127.0.0.1:9010" + title={backendErr || "Comma-separated cache gRPC addresses"} + /> + + + + {backendErr && {backendErr}} + + + +
+ + + +
+ ); +} diff --git a/examples/lab/ui/src/ChapterNav.tsx b/examples/lab/ui/src/ChapterNav.tsx new file mode 100644 index 0000000..d23a7e9 --- /dev/null +++ b/examples/lab/ui/src/ChapterNav.tsx @@ -0,0 +1,13 @@ +import { CHAPTERS } from "./api"; + +export function ChapterNav({ current, onPick }: { current: string; onPick: (id: string) => void }) { + return ( + + ); +} diff --git a/examples/lab/ui/src/Inspector.tsx b/examples/lab/ui/src/Inspector.tsx new file mode 100644 index 0000000..666234a --- /dev/null +++ b/examples/lab/ui/src/Inspector.tsx @@ -0,0 +1,63 @@ +import type { ClusterInfo, OpResp, View } from "./api"; + +export function Inspector({ + cluster, + view, + last, + name, + setName, + onRefresh, +}: { + cluster: ClusterInfo | null; + view: View | null; + last: OpResp | null; + name: string; + setName: (s: string) => void; + onRefresh: () => void; +}) { + return ( + + ); +} diff --git a/examples/lab/ui/src/Playground.tsx b/examples/lab/ui/src/Playground.tsx new file mode 100644 index 0000000..2f96d5b --- /dev/null +++ b/examples/lab/ui/src/Playground.tsx @@ -0,0 +1,93 @@ +import type { Chapter, ClusterInfo, OpResp } from "./api"; +import { BitmapWidget } from "./widgets/Bitmap"; +import { BloomWidget } from "./widgets/Bloom"; +import { CMSWidget } from "./widgets/CMS"; +import { CacheOnlyWidget } from "./widgets/CacheOnly"; +import { CounterWidget } from "./widgets/Counter"; +import { GeoWidget } from "./widgets/Geo"; +import { HLLWidget } from "./widgets/HLL"; +import { HashWidget } from "./widgets/Hash"; +import { JSONWidget } from "./widgets/JSON"; +import { ListWidget } from "./widgets/List"; +import { LoadThroughWidget } from "./widgets/LoadThrough"; +import { SetWidget } from "./widgets/Set"; +import { TopKWidget } from "./widgets/TopK"; +import { VectorSetWidget } from "./widgets/VectorSet"; +import { ZSetWidget } from "./widgets/ZSet"; +import type { WidgetProps } from "./widgets/shared"; + +export function Playground(props: { + chapter: Chapter; + via: string; + setVia: (s: string) => void; + cluster: ClusterInfo | null; + name: string; + setName: (s: string) => void; + run: (op: string, args?: Record) => Promise; + last: OpResp | null; + onScene: () => void; +}) { + const w: WidgetProps = { + name: props.name, + setName: props.setName, + via: props.via, + run: props.run, + last: props.last, + cluster: props.cluster, + }; + return ( +
+
Controls
+
+

{props.chapter.label}

+ + +
+ +
+ ); +} + +function ModeWidget({ id, w }: { id: string; w: WidgetProps }) { + switch (id) { + case "loadthrough": + return ; + case "bloom": + return ; + case "set": + return ; + case "zset": + return ; + case "geo": + return ; + case "list": + return ; + case "hash": + return ; + case "counter": + return ; + case "json": + return ; + case "bitmap": + return ; + case "hll": + return ; + case "topk": + return ; + case "cms": + return ; + case "vectorset": + return ; + default: + return ; + } +} diff --git a/examples/lab/ui/src/api.ts b/examples/lab/ui/src/api.ts new file mode 100644 index 0000000..a90b601 --- /dev/null +++ b/examples/lab/ui/src/api.ts @@ -0,0 +1,176 @@ +export type LocalKind = "missing" | "live" | "tombstone" | "negative"; + +export type NodeView = { + id: string; + cache: string; + kind: LocalKind; + version: number; + flags: number; + bytes: number; + role: "owner" | "replica" | "other"; +}; + +export type ClusterNode = { + id: string; + cache: string; + peer: string; + ready: boolean; + ring: number; +}; + +export type KeyspaceSnap = { + name: string; + mode: string; + replication_factor: number; +}; + +export type ClusterInfo = { + mode: "disconnected" | "in_process" | "remote"; + connected: boolean; + addrs: string[]; + nodes: ClusterNode[]; + keyspaces: KeyspaceSnap[]; + ring_gen: number; + rf: number; + sot_loads: number; + sot_latency: string; +}; + +export type View = { + ks: string; + key: string; + owner: string; + rf: number; + ring_gen: number; + nodes: NodeView[]; +}; + +export type OpResp = { + ok: boolean; + error?: string; + invalid_argument?: boolean; + result?: unknown; + via?: string; + owner?: string; + before?: View; + after?: View; + trace?: string[]; + sot_loads?: number; + sot_delta?: number; +}; + +export type Chapter = { + id: string; + label: string; + ks: string; + name: string; +}; + +export const CHAPTERS: Chapter[] = [ + { id: "anatomy", label: "Anatomy", ks: "cacheonly", name: "session" }, + { id: "kv-write", label: "KV write", ks: "cacheonly", name: "session" }, + { id: "loadthrough", label: "LoadThrough", ks: "loadthrough", name: "chart" }, + { id: "tombstone", label: "Tombstone", ks: "cacheonly", name: "session" }, + { id: "bloom", label: "Bloom", ks: "bloom", name: "users" }, + { id: "set", label: "Set", ks: "set", name: "flags" }, + { id: "zset", label: "ZSet", ks: "zset", name: "board" }, + { id: "geo", label: "Geo", ks: "geo", name: "places" }, + { id: "list", label: "List", ks: "list", name: "inbox" }, + { id: "hash", label: "Hash", ks: "hash", name: "profile" }, + { id: "counter", label: "Counter", ks: "counter", name: "rl" }, + { id: "json", label: "JSON", ks: "json", name: "doc" }, + { id: "bitmap", label: "Bitmap", ks: "bitmap", name: "seen" }, + { id: "hll", label: "HLL", ks: "hll", name: "uniques" }, + { id: "topk", label: "TopK", ks: "topk", name: "hot" }, + { id: "cms", label: "CMS", ks: "cms", name: "freq" }, + { id: "vectorset", label: "VectorSet", ks: "vectorset", name: "items" }, +]; + +export async function getCluster(): Promise { + const r = await fetch("/v1/cluster"); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +export async function connectBackend(opts: { addrs?: string; in_process?: boolean }): Promise { + const r = await fetch("/v1/connect", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(opts), + }); + const body = await r.json(); + if (!r.ok) throw new Error(body.error || r.statusText); + return body as ClusterInfo; +} + +export async function disconnectBackend(): Promise { + const r = await fetch("/v1/disconnect", { method: "POST" }); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +export type BloomViz = { + m: number; + k: number; + present: boolean; + bits: boolean[]; + positions: number[]; + maybe: boolean; + item: string; + name: string; +}; + +export async function getBloom(ks: string, name: string, item: string): Promise { + const q = new URLSearchParams({ ks, name, item }); + const r = await fetch(`/v1/bloom?${q}`); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +export async function getView(ks: string, key: string): Promise { + const r = await fetch(`/v1/view?ks=${encodeURIComponent(ks)}&key=${encodeURIComponent(key)}`); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +export async function runOp( + ks: string, + op: string, + name: string, + args: Record = {}, + via = "", +): Promise { + const r = await fetch("/v1/op", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ks, op, name, via, args }), + }); + const body = (await r.json()) as OpResp; + return body; +} + +export async function runScene(id: string): Promise<{ id: string; blurb: string; steps: { resp: OpResp }[] }> { + const r = await fetch(`/v1/scene/${encodeURIComponent(id)}`, { method: "POST" }); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +export async function resetLab(names: string[] = []): Promise { + await fetch("/v1/reset", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ names }), + }); +} + +export function chapterFromURL(): string { + const q = new URLSearchParams(window.location.search).get("chapter"); + if (q && CHAPTERS.some((c) => c.id === q)) return q; + return "anatomy"; +} + +export function setChapterURL(id: string) { + const u = new URL(window.location.href); + u.searchParams.set("chapter", id); + window.history.replaceState(null, "", u.toString()); +} diff --git a/examples/lab/ui/src/app.css b/examples/lab/ui/src/app.css new file mode 100644 index 0000000..bead57e --- /dev/null +++ b/examples/lab/ui/src/app.css @@ -0,0 +1,255 @@ +:root { + font-family: ui-sans-serif, system-ui, sans-serif; + color: #e8eef7; + background: #0b0f14; + line-height: 1.4; +} +* { box-sizing: border-box; } +html, body, #root { margin: 0; height: 100%; } +button, input, select, textarea { + font: inherit; + color: inherit; +} +button { + background: #38bdf8; + color: #0b0f14; + border: 0; + padding: 0.4rem 0.75rem; + border-radius: 8px; + font-weight: 600; + cursor: pointer; +} +button.ghost { + background: transparent; + color: #e8eef7; + border: 1px solid #334155; +} +button:disabled { opacity: 0.5; cursor: default; } +input, select, textarea { + background: #0f172a; + border: 1px solid #334155; + border-radius: 6px; + padding: 0.35rem 0.5rem; +} +input::placeholder, textarea::placeholder { + color: #64748b; + opacity: 1; +} +code { background: #1e293b; padding: 0.1rem 0.35rem; border-radius: 4px; font-size: 0.9em; } + +.shell { + display: grid; + grid-template-columns: 180px 1fr 580px; + grid-template-rows: auto 1fr; + grid-template-areas: + "top top top" + "nav canvas sidebar"; + height: 100%; + min-height: 0; +} +.top { + grid-area: top; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem 0.65rem; + padding: 0.55rem 1rem; + border-bottom: 1px solid #1e293b; + background: #121821; +} +.top h1 { font-size: 1.05rem; margin: 0; letter-spacing: -0.02em; white-space: nowrap; } +.muted { color: #8b9bb4; font-size: 0.85rem; } +.top .grow { flex: 1; } +.top-addrs { + flex: 1 1 18rem; + min-width: 12rem; +} +.nav { + grid-area: nav; + overflow: auto; + border-right: 1px solid #1e293b; + padding: 0.5rem 0; +} +.nav button { + display: block; + width: calc(100% - 0.8rem); + margin: 0.15rem 0.4rem; + text-align: left; + background: transparent; + color: #cbd5e1; + font-weight: 500; +} +.nav button.active { background: #1e293b; color: #7dd3fc; } +.canvas { + grid-area: canvas; + padding: 1rem; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} +.nodes { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; +} +.node { + background: #121821; + border: 1px solid #1e293b; + border-radius: 12px; + padding: 0.9rem 1rem; + min-height: 140px; +} +.node.owner { border-color: #38bdf8; box-shadow: 0 0 0 1px #38bdf833; } +.node.replica { border-color: #34d399; } +.node.tombstone { border-color: #fbbf24; } +.node.negative { border-color: #c084fc; } +.node .id { font-weight: 700; } +.node .meta { color: #8b9bb4; font-size: 0.8rem; margin-top: 0.35rem; } +.kind { + display: inline-block; + margin-top: 0.5rem; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 0.15rem 0.4rem; + border-radius: 999px; + background: #1e293b; +} +.kind.live { background: #064e3b; color: #6ee7b7; } +.kind.missing { color: #94a3b8; } +.kind.tombstone { background: #78350f; color: #fde68a; } +.kind.negative { background: #4c1d95; color: #ddd6fe; } +.trace { + background: #0f172a; + border-radius: 8px; + padding: 0.6rem 0.8rem; + font-size: 0.85rem; + color: #93c5fd; + min-height: 2.4rem; + overflow-wrap: anywhere; + word-break: break-word; +} +.sidebar { + grid-area: sidebar; + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 0; + overflow: hidden; + border-left: 1px solid #1e293b; + background: #121821; +} +.inspector { + padding: 0.85rem; + font-size: 0.9rem; + min-width: 0; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + overflow-wrap: anywhere; + word-break: break-word; +} +.inspector h2, .play > .section-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #8b9bb4; + margin: 0 0 0.5rem; +} +.inspector dl { margin: 0 0 1rem; } +.inspector dt { color: #8b9bb4; font-size: 0.75rem; } +.inspector dd { margin: 0 0 0.45rem; } +.play { + padding: 0.85rem 1rem 1rem; + min-width: 0; + min-height: 0; + overflow: auto; + border-right: 1px solid #1e293b; +} +.play h2 { margin: 0 0 0.35rem; font-size: 1rem; } +.note { color: #8b9bb4; font-size: 0.85rem; margin: 0 0 0.7rem; } +.row { display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center; margin-bottom: 0.45rem; } +.row input, .row select { min-width: 0; flex: 1 1 8rem; } +.stack { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-bottom: 0.55rem; + color: #8b9bb4; + font-size: 0.75rem; +} +.stack input { width: 100%; } +.pair { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.45rem; +} +.pair .stack { margin-bottom: 0.55rem; } +.stack-actions { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-bottom: 0.65rem; +} +.stack-actions button { width: 100%; } +.chips { display: flex; flex-wrap: wrap; gap: 0.35rem; } +.chip { + background: #1e293b; + color: #e8eef7; + font-weight: 500; + padding: 0.25rem 0.55rem; + border-radius: 999px; +} +.chip.on { background: #0e7490; color: #ecfeff; } +.bloom-hit { color: #6ee7b7; font-weight: 700; } +.bloom-miss { color: #fca5a5; font-weight: 700; } +.bits { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 4px; + max-width: 100%; +} +.bit { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; + border-radius: 4px; + background: #1e293b; + color: #64748b; + border: 1px solid #334155; + padding: 0; + font-size: 0.65rem; + font-weight: 600; +} +.bit.on { background: #38bdf8; color: #0b0f14; border-color: #7dd3fc; } +.bit.probe { box-shadow: 0 0 0 2px #fbbf24; } +.bit.probe:not(.on) { color: #fde68a; border-color: #fbbf24; } +.plot { + width: 100%; + max-width: 220px; + height: 220px; + background: #0f172a; + border-radius: 8px; + border: 1px solid #1e293b; +} +.bars { display: flex; align-items: flex-end; gap: 6px; height: 80px; } +.bar { + width: 28px; + background: #38bdf8; + border-radius: 4px 4px 0 0; + min-height: 4px; +} +pre.result { + background: #0f172a; + padding: 0.6rem; + border-radius: 8px; + overflow-x: hidden; + overflow-y: auto; + font-size: 11px; + max-height: none; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; +} diff --git a/examples/lab/ui/src/cluster/Canvas.tsx b/examples/lab/ui/src/cluster/Canvas.tsx new file mode 100644 index 0000000..36aa405 --- /dev/null +++ b/examples/lab/ui/src/cluster/Canvas.tsx @@ -0,0 +1,32 @@ +import type { OpResp, View } from "../api"; + +export function ClusterCanvas({ view, last }: { view: View | null; last: OpResp | null }) { + const nodes = view?.nodes ?? []; + return ( +
+
+ {nodes.map((n) => { + const cls = ["node", n.role, n.kind].join(" "); + return ( +
+
{n.id}
+
{n.cache}
+
+ {n.role} · v{n.version} · {n.bytes}B +
+ {n.kind} +
+ ); + })} + {nodes.length === 0 && ( +

No backend. Connect cache gRPC addresses above, or start a local 3-node mesh.

+ )} +
+
+ {(last?.trace ?? ["Run a chapter or a verb — the mesh observation lands here."]).map((line) => ( +
{line}
+ ))} +
+
+ ); +} diff --git a/examples/lab/ui/src/main.tsx b/examples/lab/ui/src/main.tsx new file mode 100644 index 0000000..fe60338 --- /dev/null +++ b/examples/lab/ui/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App"; +import "./app.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/lab/ui/src/vite-env.d.ts b/examples/lab/ui/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/examples/lab/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/lab/ui/src/widgets/Bitmap.tsx b/examples/lab/ui/src/widgets/Bitmap.tsx new file mode 100644 index 0000000..61b0321 --- /dev/null +++ b/examples/lab/ui/src/widgets/Bitmap.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function BitmapWidget({ name, setName, run, last }: WidgetProps) { + const [bits, setBits] = useState(Array(64).fill(false)); + const [offset, setOffset] = useState(0); + const result = last?.result as { bit?: boolean; present?: boolean; count?: number } | undefined; + + async function setBit(i: number, on: boolean) { + const r = await run("bitset", { offset: i, bit: on }); + if (r.ok) { + setOffset(i); + if (i < bits.length) setBits((prev) => prev.map((b, j) => (j === i ? on : b))); + } + } + + async function toggle(i: number) { + await setBit(i, !bits[i]); + } + + async function getBit(i = offset) { + setOffset(i); + await run("bitget", { offset: i }); + } + + async function wipe() { + await run("delete"); + setBits(Array(64).fill(false)); + } + + return ( +
+ + + +
+ + + + + +
+ {result?.count !== undefined &&

count = {result.count}

} + {result?.present !== undefined && result.bit !== undefined && ( +

+ offset {offset} → {result.present ? (result.bit ? "1" : "0") : "missing"} +

+ )} +

Click a cell to toggle that offset (0–63).

+
+ {bits.map((on, i) => ( + + ))} +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/Bloom.tsx b/examples/lab/ui/src/widgets/Bloom.tsx new file mode 100644 index 0000000..dc7d8e3 --- /dev/null +++ b/examples/lab/ui/src/widgets/Bloom.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState } from "react"; +import { getBloom, type BloomViz } from "../api"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function BloomWidget({ name, setName, run, last }: WidgetProps) { + const [item, setItem] = useState("alice"); + const [viz, setViz] = useState(null); + const test = last?.result as { maybe?: boolean } | undefined; + const tested = last && "maybe" in (last.result ?? {}); + + useEffect(() => { + let cancel = false; + void getBloom("bloom", name, item) + .then((v) => { + if (!cancel) setViz(v); + }) + .catch(() => { + if (!cancel) setViz(null); + }); + return () => { + cancel = true; + }; + }, [name, item, last]); + + async function add(value = item) { + const v = value.trim(); + if (!v) return; + setItem(v); + await run("bloomadd", { item: v }); + } + + async function testItem(value = item) { + const v = value.trim(); + if (!v) return; + setItem(v); + await run("bloomtest", { item: v }); + } + + async function wipe() { + await run("delete"); + } + + const bits = viz?.bits ?? Array(64).fill(false); + const probes = new Set(viz?.positions ?? []); + const maybe = tested ? test?.maybe : viz?.maybe; + + return ( +
+ + + +
+ + + +
+ {maybe !== undefined && item.trim() && ( +

+ {item} → {maybe ? "maybe" : "no"} + {viz && ` · k=${viz.k} hashes`} +

+ )} +

+ Grid is the 64-bit filter (same idea as Bitmap). Yellow ring = hash slots for the item in the box. +

+
+ {bits.map((on, i) => ( + + {i} + + ))} +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/CMS.tsx b/examples/lab/ui/src/widgets/CMS.tsx new file mode 100644 index 0000000..dc5132b --- /dev/null +++ b/examples/lab/ui/src/widgets/CMS.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function CMSWidget({ name, setName, run, last }: WidgetProps) { + const [item, setItem] = useState("t003"); + const [n, setN] = useState(1); + const count = (last?.result as { count?: number } | undefined)?.count; + return ( +
+ + + + +
+ + + {count !== undefined && {count}} +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/CacheOnly.tsx b/examples/lab/ui/src/widgets/CacheOnly.tsx new file mode 100644 index 0000000..9f8ee4d --- /dev/null +++ b/examples/lab/ui/src/widgets/CacheOnly.tsx @@ -0,0 +1,25 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function CacheOnlyWidget({ name, setName, run }: WidgetProps) { + const [value, setValue] = useState("hello"); + return ( +
+ + + +
+ + + +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/Counter.tsx b/examples/lab/ui/src/widgets/Counter.tsx new file mode 100644 index 0000000..3b3c9a8 --- /dev/null +++ b/examples/lab/ui/src/widgets/Counter.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function CounterWidget({ name, setName, run, last }: WidgetProps) { + const [delta, setDelta] = useState(1); + const n = (last?.result as { value?: number } | undefined)?.value; + return ( +
+ + + +
+ + + + {n !== undefined && {n}} +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/Geo.tsx b/examples/lab/ui/src/widgets/Geo.tsx new file mode 100644 index 0000000..e66d060 --- /dev/null +++ b/examples/lab/ui/src/widgets/Geo.tsx @@ -0,0 +1,123 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +type Hit = { member: string; lon: number; lat: number; dist_meters?: number }; + +const DOT_COLORS = ["#38bdf8", "#34d399", "#fbbf24", "#f472b6", "#a78bfa", "#fb7185", "#2dd4bf", "#f97316"]; + +function dotColor(member: string): string { + let h = 0; + for (let i = 0; i < member.length; i++) h = (h * 31 + member.charCodeAt(i)) >>> 0; + return DOT_COLORS[h % DOT_COLORS.length]; +} + +export function GeoWidget({ name, setName, run, last }: WidgetProps) { + const [member, setMember] = useState("shop"); + const [lon, setLon] = useState(-74); + const [lat, setLat] = useState(40.7); + const [radiusKm, setRadiusKm] = useState(20); + const [limit, setLimit] = useState(10); + const hits = ((last?.result as { members?: Hit[] } | undefined)?.members) ?? []; + const rKm = Math.max(radiusKm, 0.001); + const ring = 0.35; + + function meters() { + return Math.max(0, radiusKm) * 1000; + } + + function toXY(hLon: number, hLat: number) { + const kmLat = (hLat - lat) * 111.32; + const kmLon = (hLon - lon) * 111.32 * Math.cos((lat * Math.PI) / 180); + return { x: (kmLon / rKm) * ring, y: (-kmLat / rKm) * ring }; + } + + return ( +
+ + + +
+ + +
+ + +
+ + + +
+ {hits.length > 0 && ( +

+ {hits.length} hit{hits.length === 1 ? "" : "s"} within {radiusKm} km +

+ )} + + + {hits.map((h) => { + const p = toXY(h.lon, h.lat); + return ( + + ); + })} + +
+ ); +} diff --git a/examples/lab/ui/src/widgets/HLL.tsx b/examples/lab/ui/src/widgets/HLL.tsx new file mode 100644 index 0000000..9bbc6bf --- /dev/null +++ b/examples/lab/ui/src/widgets/HLL.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function HLLWidget({ name, setName, run, last }: WidgetProps) { + const [item, setItem] = useState("user-1"); + const [exact, setExact] = useState>(new Set()); + const est = (last?.result as { count?: number } | undefined)?.count; + return ( +
+ + + +
+ + +
+

+ estimate {est ?? "—"} · exact UI set {exact.size} +

+
+ ); +} diff --git a/examples/lab/ui/src/widgets/Hash.tsx b/examples/lab/ui/src/widgets/Hash.tsx new file mode 100644 index 0000000..82339e4 --- /dev/null +++ b/examples/lab/ui/src/widgets/Hash.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function HashWidget({ name, setName, run, last }: WidgetProps) { + const [field, setField] = useState("email"); + const [value, setValue] = useState("a@b"); + const fields = ((last?.result as { fields?: { field: string; value: string }[] } | undefined)?.fields) ?? []; + return ( +
+ + + + +
+ + + + +
+ {fields.map((f) => ( +
+ {f.field} = {f.value} +
+ ))} +
+ ); +} diff --git a/examples/lab/ui/src/widgets/JSON.tsx b/examples/lab/ui/src/widgets/JSON.tsx new file mode 100644 index 0000000..68220d5 --- /dev/null +++ b/examples/lab/ui/src/widgets/JSON.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function JSONWidget({ name, setName, run, last }: WidgetProps) { + const [path, setPath] = useState("$.name"); + const [value, setValue] = useState('"Ada"'); + const raw = (last?.result as { raw?: string } | undefined)?.raw; + return ( +
+ + + + +
+ + + +
+ {raw &&
{raw}
} +
+ ); +} diff --git a/examples/lab/ui/src/widgets/List.tsx b/examples/lab/ui/src/widgets/List.tsx new file mode 100644 index 0000000..6d9bfc8 --- /dev/null +++ b/examples/lab/ui/src/widgets/List.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function ListWidget({ name, setName, run, last }: WidgetProps) { + const [item, setItem] = useState("event1"); + const items = ((last?.result as { items?: string[] } | undefined)?.items) ?? []; + return ( +
+ + + +
+ + + + + +
+
+ {items.map((it, i) => ( + {it} + ))} +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/LoadThrough.tsx b/examples/lab/ui/src/widgets/LoadThrough.tsx new file mode 100644 index 0000000..5ba9b42 --- /dev/null +++ b/examples/lab/ui/src/widgets/LoadThrough.tsx @@ -0,0 +1,17 @@ +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function LoadThroughWidget({ name, setName, run, cluster }: WidgetProps) { + return ( +
+ + +
+ + + SoT loads: {cluster?.sot_loads ?? 0} +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/Set.tsx b/examples/lab/ui/src/widgets/Set.tsx new file mode 100644 index 0000000..6c0f9b0 --- /dev/null +++ b/examples/lab/ui/src/widgets/Set.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function SetWidget({ name, setName, run, last }: WidgetProps) { + const [item, setItem] = useState("dark_mode"); + const members = ((last?.result as { members?: string[] } | undefined)?.members) ?? []; + return ( +
+ + + +
+ + + + + +
+ {members.length > 0 &&

{members.join(", ")}

} +
+ ); +} diff --git a/examples/lab/ui/src/widgets/TopK.tsx b/examples/lab/ui/src/widgets/TopK.tsx new file mode 100644 index 0000000..af4248a --- /dev/null +++ b/examples/lab/ui/src/widgets/TopK.tsx @@ -0,0 +1,29 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function TopKWidget({ name, setName, run, last }: WidgetProps) { + const [item, setItem] = useState("t001"); + const entries = ((last?.result as { entries?: { item: string; count: number }[] } | undefined)?.entries) ?? []; + const max = Math.max(1, ...entries.map((e) => e.count)); + return ( +
+ + + +
+ + +
+
+ {entries.map((e) => ( +
+ ))} +
+
+ ); +} diff --git a/examples/lab/ui/src/widgets/VectorSet.tsx b/examples/lab/ui/src/widgets/VectorSet.tsx new file mode 100644 index 0000000..fcbc420 --- /dev/null +++ b/examples/lab/ui/src/widgets/VectorSet.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +type Pt = { member: string; x: number; y: number }; + +export function VectorSetWidget({ name, setName, run, last }: WidgetProps) { + const [member, setMember] = useState("east"); + const [x, setX] = useState(1); + const [y, setY] = useState(0); + const [pts, setPts] = useState([]); + const hits = ((last?.result as { hits?: { member: string; score: number }[] } | undefined)?.hits) ?? []; + return ( +
+ + + + + +
+ + + +
+ + + + {pts.map((p) => ( + + ))} + + + {hits.length > 0 &&

{hits.map((h) => `${h.member} ${h.score.toFixed(3)}`).join(" · ")}

} +
+ ); +} diff --git a/examples/lab/ui/src/widgets/ZSet.tsx b/examples/lab/ui/src/widgets/ZSet.tsx new file mode 100644 index 0000000..acf0897 --- /dev/null +++ b/examples/lab/ui/src/widgets/ZSet.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { NameRow, Note, type WidgetProps } from "./shared"; + +export function ZSetWidget({ name, setName, run, last }: WidgetProps) { + const [member, setMember] = useState("alice"); + const [score, setScore] = useState(100); + const rows = ((last?.result as { members?: { member: string; score: number }[] } | undefined)?.members) ?? []; + return ( +
+ + + + +
+ + + +
+ {rows.length > 0 && ( + + + {rows.map((r) => ( + + + + + ))} + +
{r.member}{r.score}
+ )} +
+ ); +} diff --git a/examples/lab/ui/src/widgets/shared.tsx b/examples/lab/ui/src/widgets/shared.tsx new file mode 100644 index 0000000..29537d0 --- /dev/null +++ b/examples/lab/ui/src/widgets/shared.tsx @@ -0,0 +1,35 @@ +import type { ClusterInfo, OpResp } from "../api"; + +export type WidgetProps = { + name: string; + setName: (s: string) => void; + via: string; + run: (op: string, args?: Record) => Promise; + last: OpResp | null; + cluster: ClusterInfo | null; +}; + +export function Note({ when, not }: { when: string; not: string }) { + return ( +

+ Use when {when} · Not {not} +

+ ); +} + +export function NameRow({ + name, + setName, + placeholder = "structure name, e.g. session", +}: { + name: string; + setName: (s: string) => void; + placeholder?: string; +}) { + return ( + + ); +} diff --git a/examples/lab/ui/tsconfig.json b/examples/lab/ui/tsconfig.json new file mode 100644 index 0000000..109f0ac --- /dev/null +++ b/examples/lab/ui/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/lab/ui/vite.config.ts b/examples/lab/ui/vite.config.ts new file mode 100644 index 0000000..89bebac --- /dev/null +++ b/examples/lab/ui/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { "/v1": "http://127.0.0.1:19080" }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + }, +}); diff --git a/examples/lab/walkthrough.go b/examples/lab/walkthrough.go new file mode 100644 index 0000000..bbae3b6 --- /dev/null +++ b/examples/lab/walkthrough.go @@ -0,0 +1,85 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +func runWalkthrough(out io.Writer) error { + lab, err := startLab(labConfig{HTTPAddr: "127.0.0.1:0", SoTLatency: 20 * time.Millisecond, InProcess: true}) + if err != nil { + return err + } + defer lab.Close() + base := "http://" + lab.Addr + p := func(format string, args ...any) { fmt.Fprintf(out, format+"\n", args...) } + p("SuperCache Lab walkthrough cluster=%s", base) + + code, body, err := httpGet(base + "/v1/cluster") + if err != nil { + return err + } + if code != 200 { + return fmt.Errorf("cluster HTTP %d %s", code, body) + } + var cl map[string]any + if err := json.Unmarshal(body, &cl); err != nil { + return err + } + nodes, _ := cl["nodes"].([]any) + if len(nodes) != 3 { + return fmt.Errorf("want 3 nodes, got %d", len(nodes)) + } + p(" /v1/cluster nodes=%d sot_loads=%v", len(nodes), cl["sot_loads"]) + + code, body, err = httpPost(base+"/v1/op", map[string]any{ + "ks": "set", "op": "get", "name": "flags", + }) + if err != nil { + return err + } + if code != http.StatusBadRequest { + return fmt.Errorf("wrong verb want 400, got %d %s", code, body) + } + p(" Get on ModeSet → HTTP %d (invalid argument)", code) + + code, body, err = httpPost(base+"/v1/scene/kv-write", map[string]any{}) + if err != nil { + return err + } + if code != 200 { + return fmt.Errorf("scene kv-write HTTP %d %s", code, body) + } + p(" scene kv-write ok") + + p("OK: SuperCache Lab walkthrough passed") + return nil +} + +func httpGet(url string) (int, []byte, error) { + resp, err := http.Get(url) + if err != nil { + return 0, nil, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + return resp.StatusCode, b, err +} + +func httpPost(url string, payload map[string]any) (int, []byte, error) { + raw, err := json.Marshal(payload) + if err != nil { + return 0, nil, err + } + resp, err := http.Post(url, "application/json", bytes.NewReader(raw)) + if err != nil { + return 0, nil, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + return resp.StatusCode, b, err +} diff --git a/pkg/bloom/bloom.go b/pkg/bloom/bloom.go index 04c0efb..5110d74 100644 --- a/pkg/bloom/bloom.go +++ b/pkg/bloom/bloom.go @@ -89,6 +89,23 @@ func (f *Filter) Bytes() []byte { return f.bits } +// Indexes returns the k bit positions item hashes to in an m-bit filter. +func Indexes(mBits, k int, item []byte) []int { + if mBits < 8 { + mBits = 8 + } + if k < 1 { + k = 1 + } + h1, h2 := hash2(item) + m := uint64(mBits) + out := make([]int, k) + for i := 0; i < k; i++ { + out[i] = int((h1 + uint64(i)*h2) % m) + } + return out +} + func hash2(item []byte) (h1, h2 uint64) { a := fnv.New64a() _, _ = a.Write(item) diff --git a/pkg/engine/localview_test.go b/pkg/engine/localview_test.go new file mode 100644 index 0000000..d9d8968 --- /dev/null +++ b/pkg/engine/localview_test.go @@ -0,0 +1,105 @@ +package engine_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/Code0987/supercache/pkg/datasource" + "github.com/Code0987/supercache/pkg/engine" + "github.com/Code0987/supercache/pkg/keyspace" + "github.com/Code0987/supercache/pkg/store" +) + +func TestLocalViewKinds(t *testing.T) { + e := engine.New() + defer e.Close() + src := datasource.Func(func(_ context.Context, key string) ([]byte, error) { + if key == "missing" { + return nil, datasource.ErrNotFound + } + return []byte("v"), nil + }) + if err := e.UpdateKeySpace(keyspace.Config{ + Name: "lt", Mode: keyspace.ModeLoadThrough, MaxBytes: 1 << 20, TTL: time.Minute, + NegativeTTL: time.Minute, DataSource: src, + }); err != nil { + t.Fatal(err) + } + if err := e.UpdateKeySpace(keyspace.Config{ + Name: "c", Mode: keyspace.ModeCacheOnly, MaxBytes: 1 << 20, TTL: time.Minute, + }); err != nil { + t.Fatal(err) + } + ctx := context.Background() + + miss := e.LocalView("c", "nope") + if miss.Kind != engine.LocalMissing { + t.Fatalf("absent key: %+v", miss) + } + if unknown := e.LocalView("nosuch", "k"); unknown.Kind != engine.LocalMissing { + t.Fatalf("unknown keyspace: %+v", unknown) + } + + if err := e.Put(ctx, "c", "k", []byte("hello")); err != nil { + t.Fatal(err) + } + live := e.LocalView("c", "k") + if live.Kind != engine.LocalLive || live.Bytes != 5 || live.Version == 0 { + t.Fatalf("live: %+v", live) + } + if live.Flags&store.FlagTombstone != 0 || live.Flags&store.FlagNegative != 0 { + t.Fatalf("live flags: %+v", live) + } + + if err := e.Delete(ctx, "c", "k"); err != nil { + t.Fatal(err) + } + tomb := e.LocalView("c", "k") + if tomb.Kind != engine.LocalTombstone || tomb.Version == 0 { + t.Fatalf("tombstone: %+v", tomb) + } + + _, err := e.Get(ctx, "lt", "missing") + if !errors.Is(err, engine.ErrNotFound) { + t.Fatalf("want not found, got %v", err) + } + neg := e.LocalView("lt", "missing") + if neg.Kind != engine.LocalNegative { + t.Fatalf("negative: %+v", neg) + } +} + +func TestBloomDump(t *testing.T) { + e := engine.New() + defer e.Close() + if err := e.UpdateKeySpace(keyspace.Config{ + Name: "bf", Mode: keyspace.ModeBloom, MaxBytes: 1 << 20, + BloomBits: 64, BloomHashes: 4, + }); err != nil { + t.Fatal(err) + } + _, m, k, ok := e.BloomDump("bf", "users") + if ok || m != 64 || k != 4 { + t.Fatalf("missing: ok=%v m=%d k=%d", ok, m, k) + } + if err := e.BloomAdd(context.Background(), "bf", "users", []byte("alice")); err != nil { + t.Fatal(err) + } + bits, m, k, ok := e.BloomDump("bf", "users") + if !ok || m != 64 || k != 4 || len(bits) != 8 { + t.Fatalf("dump: ok=%v m=%d k=%d len=%d", ok, m, k, len(bits)) + } + ones := 0 + for _, b := range bits { + for i := 0; i < 8; i++ { + if b&(1<