first commit
This commit is contained in:
commit
644b536a49
19
README.md
Normal file
19
README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Dragon Knight
|
||||
|
||||
A hyper-performant version of Dragon Knight built with cool technologies.
|
||||
|
||||
# Backend
|
||||
|
||||
We're using Go! Since Dragon Knight is a game that's purely SSR, we opt to use the best library for this purpose; fasthttp. We have our own custom radix tree router to dispatch, and all events are handled on the server with speed. To avoid difficulty while in development, though, all our static assets and non-database data are in a folder adjacent to the executable, so we lose the advantage of embedding but gain dev speed.
|
||||
|
||||
# Database
|
||||
|
||||
Dragon Knight's database is small and generally doesn't see many writes. Therefore we can use a single monolith SQLite file as the database. We still use WAL and give it extra memory - performance is a blessing!
|
||||
|
||||
Because our backend language has no databases built-in, we have to bring in a dependency for this one - it is what it is. For this purpose, we use the CGO-free zombiezen library - which also tends to be among the most performant SQLite wrappers in Go.
|
||||
|
||||
To make life easier, we use an Active Record kind of pattern for database objects.
|
||||
|
||||
# Frontend
|
||||
|
||||
Pure HTML, CSS and JavaScript. We enhance the frontend with AJAX where applicable. Again, these assets are adjacent to the executable for dev speed.
|
7
cmd/main.go
Normal file
7
cmd/main.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Main!")
|
||||
}
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module dk
|
||||
|
||||
go 1.24.6
|
||||
|
||||
require github.com/valyala/fasthttp v1.64.0
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
)
|
10
go.sum
Normal file
10
go.sum
Normal file
@ -0,0 +1,10 @@
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
|
||||
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
435
internal/router/router.go
Normal file
435
internal/router/router.go
Normal file
@ -0,0 +1,435 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type Ctx = *fasthttp.RequestCtx
|
||||
|
||||
// Handler is a request handler with parameters.
|
||||
type Handler func(ctx Ctx, params []string)
|
||||
|
||||
func (h Handler) Serve(ctx Ctx, params []string) {
|
||||
h(ctx, params)
|
||||
}
|
||||
|
||||
type Middleware func(Handler) Handler
|
||||
|
||||
type node struct {
|
||||
segment string
|
||||
handler Handler
|
||||
children []*node
|
||||
isDynamic bool
|
||||
isWildcard bool
|
||||
maxParams uint8
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
get *node
|
||||
post *node
|
||||
put *node
|
||||
patch *node
|
||||
delete *node
|
||||
middleware []Middleware
|
||||
paramsBuffer []string // Pre-allocated buffer for parameters
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
router *Router
|
||||
prefix string
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
// Creates a new Router instance.
|
||||
func New() *Router {
|
||||
return &Router{
|
||||
get: &node{},
|
||||
post: &node{},
|
||||
put: &node{},
|
||||
patch: &node{},
|
||||
delete: &node{},
|
||||
middleware: []Middleware{},
|
||||
paramsBuffer: make([]string, 64),
|
||||
}
|
||||
}
|
||||
|
||||
// Implements the Handler interface for fasthttp
|
||||
func (r *Router) ServeHTTP(ctx *fasthttp.RequestCtx) {
|
||||
path := string(ctx.Path())
|
||||
method := string(ctx.Method())
|
||||
|
||||
h, params, found := r.Lookup(method, path)
|
||||
if !found {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h(ctx, params)
|
||||
}
|
||||
|
||||
// Returns a fasthttp request handler
|
||||
func (r *Router) Handler() fasthttp.RequestHandler {
|
||||
return r.ServeHTTP
|
||||
}
|
||||
|
||||
// Adds middleware to the router.
|
||||
func (r *Router) Use(mw ...Middleware) *Router {
|
||||
r.middleware = append(r.middleware, mw...)
|
||||
return r
|
||||
}
|
||||
|
||||
// Creates a new route group.
|
||||
func (r *Router) Group(prefix string) *Group {
|
||||
return &Group{router: r, prefix: prefix, middleware: []Middleware{}}
|
||||
}
|
||||
|
||||
// Adds middleware to the group.
|
||||
func (g *Group) Use(mw ...Middleware) *Group {
|
||||
g.middleware = append(g.middleware, mw...)
|
||||
return g
|
||||
}
|
||||
|
||||
// Creates a nested group.
|
||||
func (g *Group) Group(prefix string) *Group {
|
||||
return &Group{
|
||||
router: g.router,
|
||||
prefix: g.prefix + prefix,
|
||||
middleware: append([]Middleware{}, g.middleware...),
|
||||
}
|
||||
}
|
||||
|
||||
// Applies middleware in reverse order.
|
||||
func applyMiddleware(h Handler, mw []Middleware) Handler {
|
||||
for i := len(mw) - 1; i >= 0; i-- {
|
||||
h = mw[i](h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Registers a handler for the given method and path.
|
||||
func (r *Router) Handle(method, path string, h Handler) error {
|
||||
root := r.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return r.addRoute(root, path, h, r.middleware)
|
||||
}
|
||||
|
||||
func (r *Router) methodNode(method string) *node {
|
||||
switch method {
|
||||
case "GET":
|
||||
return r.get
|
||||
case "POST":
|
||||
return r.post
|
||||
case "PUT":
|
||||
return r.put
|
||||
case "PATCH":
|
||||
return r.patch
|
||||
case "DELETE":
|
||||
return r.delete
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Registers a GET handler.
|
||||
func (r *Router) Get(path string, h Handler) error {
|
||||
return r.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler.
|
||||
func (r *Router) Post(path string, h Handler) error {
|
||||
return r.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler.
|
||||
func (r *Router) Put(path string, h Handler) error {
|
||||
return r.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler.
|
||||
func (r *Router) Patch(path string, h Handler) error {
|
||||
return r.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler.
|
||||
func (r *Router) Delete(path string, h Handler) error {
|
||||
return r.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
func (g *Group) buildGroupMiddleware() []Middleware {
|
||||
mw := append([]Middleware{}, g.router.middleware...)
|
||||
return append(mw, g.middleware...)
|
||||
}
|
||||
|
||||
// Registers a handler in the group.
|
||||
func (g *Group) Handle(method, path string, h Handler) error {
|
||||
root := g.router.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return g.router.addRoute(root, g.prefix+path, h, g.buildGroupMiddleware())
|
||||
}
|
||||
|
||||
// Registers a GET handler in the group.
|
||||
func (g *Group) Get(path string, h Handler) error {
|
||||
return g.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler in the group.
|
||||
func (g *Group) Post(path string, h Handler) error {
|
||||
return g.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler in the group.
|
||||
func (g *Group) Put(path string, h Handler) error {
|
||||
return g.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler in the group.
|
||||
func (g *Group) Patch(path string, h Handler) error {
|
||||
return g.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler in the group.
|
||||
func (g *Group) Delete(path string, h Handler) error {
|
||||
return g.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
// Applies specific middleware for next registration.
|
||||
func (r *Router) WithMiddleware(mw ...Middleware) *MiddlewareRouter {
|
||||
return &MiddlewareRouter{router: r, middleware: mw}
|
||||
}
|
||||
|
||||
// Applies specific middleware for next group route.
|
||||
func (g *Group) WithMiddleware(mw ...Middleware) *MiddlewareGroup {
|
||||
return &MiddlewareGroup{group: g, middleware: mw}
|
||||
}
|
||||
|
||||
type MiddlewareRouter struct {
|
||||
router *Router
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
type MiddlewareGroup struct {
|
||||
group *Group
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
func (mr *MiddlewareRouter) buildMiddleware() []Middleware {
|
||||
mw := append([]Middleware{}, mr.router.middleware...)
|
||||
return append(mw, mr.middleware...)
|
||||
}
|
||||
|
||||
// Registers a handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Handle(method, path string, h Handler) error {
|
||||
root := mr.router.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return mr.router.addRoute(root, path, h, mr.buildMiddleware())
|
||||
}
|
||||
|
||||
// Registers a GET handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Get(path string, h Handler) error {
|
||||
return mr.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Post(path string, h Handler) error {
|
||||
return mr.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Put(path string, h Handler) error {
|
||||
return mr.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Patch(path string, h Handler) error {
|
||||
return mr.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Delete(path string, h Handler) error {
|
||||
return mr.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
func (mg *MiddlewareGroup) buildMiddleware() []Middleware {
|
||||
mw := append([]Middleware{}, mg.group.router.middleware...)
|
||||
mw = append(mw, mg.group.middleware...)
|
||||
return append(mw, mg.middleware...)
|
||||
}
|
||||
|
||||
// Registers a handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Handle(method, path string, h Handler) error {
|
||||
root := mg.group.router.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return mg.group.router.addRoute(root, mg.group.prefix+path, h, mg.buildMiddleware())
|
||||
}
|
||||
|
||||
// Registers a GET handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Get(path string, h Handler) error {
|
||||
return mg.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Post(path string, h Handler) error {
|
||||
return mg.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Put(path string, h Handler) error {
|
||||
return mg.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Patch(path string, h Handler) error {
|
||||
return mg.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Delete(path string, h Handler) error {
|
||||
return mg.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
// Adapts a standard fasthttp.RequestHandler to the router's Handler
|
||||
func StandardHandler(handler fasthttp.RequestHandler) Handler {
|
||||
return func(ctx Ctx, _ []string) {
|
||||
handler(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts the next path segment.
|
||||
func readSegment(path string, start int) (segment string, end int, hasMore bool) {
|
||||
if start >= len(path) {
|
||||
return "", start, false
|
||||
}
|
||||
if path[start] == '/' {
|
||||
start++
|
||||
}
|
||||
if start >= len(path) {
|
||||
return "", start, false
|
||||
}
|
||||
end = start
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
return path[start:end], end, end < len(path)
|
||||
}
|
||||
|
||||
// Adds a new route to the trie.
|
||||
func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) error {
|
||||
h = applyMiddleware(h, mw)
|
||||
if path == "/" {
|
||||
root.handler = h
|
||||
return nil
|
||||
}
|
||||
current := root
|
||||
pos := 0
|
||||
lastWC := false
|
||||
count := uint8(0)
|
||||
for {
|
||||
seg, newPos, more := readSegment(path, pos)
|
||||
if seg == "" {
|
||||
break
|
||||
}
|
||||
isDyn := len(seg) > 1 && seg[0] == ':'
|
||||
isWC := len(seg) > 0 && seg[0] == '*'
|
||||
if isWC {
|
||||
if lastWC || more {
|
||||
return fmt.Errorf("wildcard must be the last segment in the path")
|
||||
}
|
||||
lastWC = true
|
||||
}
|
||||
if isDyn || isWC {
|
||||
count++
|
||||
}
|
||||
var child *node
|
||||
for _, c := range current.children {
|
||||
if c.segment == seg {
|
||||
child = c
|
||||
break
|
||||
}
|
||||
}
|
||||
if child == nil {
|
||||
child = &node{segment: seg, isDynamic: isDyn, isWildcard: isWC}
|
||||
current.children = append(current.children, child)
|
||||
}
|
||||
if child.maxParams < count {
|
||||
child.maxParams = count
|
||||
}
|
||||
current = child
|
||||
pos = newPos
|
||||
}
|
||||
current.handler = h
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finds a handler matching method and path.
|
||||
func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
|
||||
root := r.methodNode(method)
|
||||
if root == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
if path == "/" {
|
||||
return root.handler, nil, root.handler != nil
|
||||
}
|
||||
|
||||
buffer := r.paramsBuffer
|
||||
if cap(buffer) < int(root.maxParams) {
|
||||
buffer = make([]string, root.maxParams)
|
||||
r.paramsBuffer = buffer
|
||||
}
|
||||
buffer = buffer[:0]
|
||||
|
||||
h, paramCount, found := match(root, path, 0, &buffer)
|
||||
if !found {
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
return h, buffer[:paramCount], true
|
||||
}
|
||||
|
||||
// Traverses the trie to find a handler.
|
||||
func match(current *node, path string, start int, params *[]string) (Handler, int, bool) {
|
||||
paramCount := 0
|
||||
|
||||
for _, c := range current.children {
|
||||
if c.isWildcard {
|
||||
rem := path[start:]
|
||||
if len(rem) > 0 && rem[0] == '/' {
|
||||
rem = rem[1:]
|
||||
}
|
||||
*params = append(*params, rem)
|
||||
return c.handler, 1, c.handler != nil
|
||||
}
|
||||
}
|
||||
|
||||
seg, pos, more := readSegment(path, start)
|
||||
if seg == "" {
|
||||
return current.handler, 0, current.handler != nil
|
||||
}
|
||||
|
||||
for _, c := range current.children {
|
||||
if c.segment == seg || c.isDynamic {
|
||||
if c.isDynamic {
|
||||
*params = append(*params, seg)
|
||||
paramCount++
|
||||
}
|
||||
if !more {
|
||||
return c.handler, paramCount, c.handler != nil
|
||||
}
|
||||
h, nestedCount, ok := match(c, path, pos, params)
|
||||
if ok {
|
||||
return h, paramCount + nestedCount, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, 0, false
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user