Files
x0gp/server/smoke_ws.go
T
x0gp 978d36c505 feat(server): initial implementation
This commit captures the full server code accumulated across
several development sessions. It includes the following layers,
applied in roughly this order during development:

1. Base infrastructure
   - cmd/poc-server HTTP+WebSocket server, /health, /stats
   - internal/transport wire types (Envelope + all message types)
   - internal/catalog, internal/realtime, internal/stats
   - internal/storage/postgres with migrations 001-004
   - .env, .env.example, docker-compose, Dockerfile, scripts/

2. Swagger documentation
   - go get github.com/swaggo/http-swagger
   - swag init produces docs/docs.go
   - /swagger/index.html UI, /swagger/doc.json spec
   - annotations on health/stats/ws and catalog handlers

3. Races API
   - internal/races/{store,service,keyset,types}.go
   - migration 005_races.sql: finished_races, race_plans, race_queue
   - GET /api/races with keyset pagination, status filter
   - GET /api/races/upcoming
   - POST /api/races/queue/join, GET, DELETE
   - POST/GET /api/races/plans, DELETE /{id}
   - lobby.RaceMeta simplification (no host_id/host_name)

4. Races seeder
   - internal/races/seed with deterministic generator
   - --seed-races / --reset CLI flags in main.go
   - 30 finished + 5 live + 5 plans + 4 queue entries

5. Drivers and clans
   - internal/drivers, internal/clans (CRUD, validation)
   - migration 008_drivers_clans.sql
   - /api/clans and /api/drivers endpoints
   - 3-letter uppercase nickname/tag validation
   - lobby.DriverMeta: Nickname, AvatarURL, ClanID, ClanTag

6. Podium
   - internal/transport.RacePodiumEntry
   - lobby.SetDriverProfile for in-memory metadata sync
   - migration 007_podium.sql (podium JSONB column)
   - seeder populates top-3 per finished race

7. Live races persistence
   - migration 009_live_persistence.sql: live_races,
     live_race_drivers, lobby_drivers
   - internal/races/live_store.go: LiveStore with write-side
     mirror for lobby.Service mutations
   - Service.collectLive and Upcoming read from Postgres
   - main.go RestoreFromDB rehydrates lobby on startup

8. Unify live + finished into one races table
   - migration 010_unify_races.sql: rename finished_races to
     races, expand status CHECK, merge live rows
   - PgStore now hosts both write paths; LiveStore is a
     thin facade implementing lobby.Persistence
   - seeder resetAll drops only finished/cancelled rows and
     race_plans / race_queue / lobby_drivers

Each layer is consistent on its own; cross-layer changes are
visible in the file history. A future refactor may split this
commit into the per-stage boundaries listed above.
2026-06-22 22:01:09 +04:00

96 lines
2.3 KiB
Go

//go:build ignore
// +build ignore
// Smoke test: ws client connects, exchanges a few messages, exits.
// Run: go run ./smoke_test.go
package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"time"
"github.com/gorilla/websocket"
)
func main() {
u := url.URL{Scheme: "ws", Host: "localhost:8080", Path: "/ws"}
ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
fmt.Println("dial:", err)
os.Exit(1)
}
defer ws.Close()
// channel reader
go func() {
for {
_, msg, err := ws.ReadMessage()
if err != nil {
fmt.Println("read err:", err)
return
}
var env struct {
Type string `json:"type"`
Seq uint64 `json:"seq"`
TSMs int64 `json:"ts_ms"`
Payload json.RawMessage `json:"payload"`
}
if err := json.Unmarshal(msg, &env); err != nil {
fmt.Println("json err:", err)
continue
}
if env.Type == "race_snapshot" {
fmt.Printf("[rx] %s tick=%d cars=%d ts=%d\n", env.Type, 0, countCars(env.Payload), env.TSMs)
} else {
fmt.Printf("[rx] %s seq=%d payload=%s\n", env.Type, env.Seq, string(env.Payload))
}
}
}()
send := func(t string, p map[string]any) {
env := map[string]any{
"type": t,
"seq": 1,
"ts_ms": time.Now().UnixMilli(),
"payload": p,
}
b, _ := json.Marshal(env)
if err := ws.WriteMessage(websocket.TextMessage, b); err != nil {
fmt.Println("write err:", err)
}
fmt.Println("[tx]", t)
}
send("client_hello", map[string]any{
"version": "x0gp/0.1", "token": "", "locale": "ru",
})
time.Sleep(200 * time.Millisecond)
send("join_race", map[string]any{
"race_id": "demo-race", "car_slot": 0, "car_name": "smoke-test",
})
time.Sleep(200 * time.Millisecond)
for i := 0; i < 30; i++ {
send("input_state", map[string]any{
"steering": 0.5, "throttle": 1.0, "brake": 0.0, "gear": 1,
})
time.Sleep(33 * time.Millisecond)
}
send("ping", map[string]any{"client_ts_ms": time.Now().UnixMilli()})
time.Sleep(200 * time.Millisecond)
send("leave_race", map[string]any{})
// give server time to push last snapshots
time.Sleep(500 * time.Millisecond)
fmt.Println("done")
}
func countCars(raw json.RawMessage) int {
var s struct {
Cars []map[string]any `json:"cars"`
}
_ = json.Unmarshal(raw, &s)
return len(s.Cars)
}