diff --git a/http/server.go b/http/server.go index 575fe8f..424e1a3 100644 --- a/http/server.go +++ b/http/server.go @@ -1,11 +1,12 @@ package http import ( + "bytes" "context" "sync" "time" - "Moonshark/routers" + "Moonshark/router" "Moonshark/runner" "Moonshark/sessions" "Moonshark/utils" @@ -17,52 +18,113 @@ import ( "github.com/valyala/fasthttp" ) +var ( + //methodGET = []byte("GET") + methodPOST = []byte("POST") + methodPUT = []byte("PUT") + methodPATCH = []byte("PATCH") + debugPath = []byte("/debug/stats") +) + type Server struct { - luaRouter *routers.LuaRouter - staticRouter *routers.StaticRouter - luaRunner *runner.Runner - fasthttpServer *fasthttp.Server - loggingEnabled bool - debugMode bool - config *config.Config - sessionManager *sessions.SessionManager - errorConfig utils.ErrorPageConfig - ctxPool sync.Pool + luaRouter *router.LuaRouter + staticHandler fasthttp.RequestHandler + staticFS *fasthttp.FS + luaRunner *runner.Runner + fasthttpServer *fasthttp.Server + loggingEnabled bool + debugMode bool + config *config.Config + sessionManager *sessions.SessionManager + errorConfig utils.ErrorPageConfig + ctxPool sync.Pool + paramsPool sync.Pool + staticDir string + staticPrefix string + staticPrefixBytes []byte + + // Cached error pages + cached404 []byte + cached500 []byte + errorCacheMu sync.RWMutex } -func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, +func New(luaRouter *router.LuaRouter, staticDir string, runner *runner.Runner, loggingEnabled bool, debugMode bool, overrideDir string, config *config.Config) *Server { + staticPrefix := config.Server.StaticPrefix + if staticPrefix == "" { + staticPrefix = "/static/" + } + + if staticPrefix[0] != '/' { + staticPrefix = "/" + staticPrefix + } + if staticPrefix[len(staticPrefix)-1] != '/' { + staticPrefix = staticPrefix + "/" + } + s := &Server{ - luaRouter: luaRouter, - staticRouter: staticRouter, - luaRunner: runner, - loggingEnabled: loggingEnabled, - debugMode: debugMode, - config: config, - sessionManager: sessions.GlobalSessionManager, + luaRouter: luaRouter, + luaRunner: runner, + loggingEnabled: loggingEnabled, + debugMode: debugMode, + config: config, + sessionManager: sessions.GlobalSessionManager, + staticDir: staticDir, + staticPrefix: staticPrefix, + staticPrefixBytes: []byte(staticPrefix), errorConfig: utils.ErrorPageConfig{ OverrideDir: overrideDir, DebugMode: debugMode, }, ctxPool: sync.Pool{ New: func() any { - return make(map[string]any, 8) + return make(map[string]any, 6) + }, + }, + paramsPool: sync.Pool{ + New: func() any { + return make(map[string]any, 4) }, }, } + // Pre-cache error pages + s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, "")) + s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error")) + + // Setup static file serving + if staticDir != "" { + s.staticFS = &fasthttp.FS{ + Root: staticDir, + IndexNames: []string{"index.html"}, + GenerateIndexPages: false, + AcceptByteRange: true, + Compress: true, + CompressedFileSuffix: ".gz", + CompressBrotli: true, + CompressZstd: true, + PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1), + } + s.staticHandler = s.staticFS.NewRequestHandler() + } + s.fasthttpServer = &fasthttp.Server{ - Handler: s.handleRequest, - Name: "Moonshark/" + metadata.Version, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - MaxRequestBodySize: 16 << 20, - TCPKeepalive: true, - TCPKeepalivePeriod: 60 * time.Second, - ReduceMemoryUsage: true, - DisablePreParseMultipartForm: true, + Handler: s.handleRequest, + Name: "Moonshark/" + metadata.Version, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + MaxRequestBodySize: 16 << 20, + TCPKeepalive: true, + TCPKeepalivePeriod: 60 * time.Second, + ReduceMemoryUsage: true, + DisablePreParseMultipartForm: true, + DisableHeaderNamesNormalizing: true, + NoDefaultServerHeader: true, + StreamRequestBody: true, } return s @@ -79,56 +141,48 @@ func (s *Server) Shutdown(ctx context.Context) error { func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) { start := time.Now() - method := string(ctx.Method()) - path := string(ctx.Path()) + methodBytes := ctx.Method() + pathBytes := ctx.Path() - if s.debugMode && path == "/debug/stats" { + // Fast path for debug stats + if s.debugMode && bytes.Equal(pathBytes, debugPath) { s.handleDebugStats(ctx) if s.loggingEnabled { - logger.LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start)) + logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start)) } return } - logger.Debug("Processing request %s %s", method, path) + // Fast path for static files + if s.staticHandler != nil && bytes.HasPrefix(pathBytes, s.staticPrefixBytes) { + s.staticHandler(ctx) + if s.loggingEnabled { + logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start)) + } + return + } - params := &routers.Params{} - bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params) + // Lua route lookup - only allocate params if found + bytecode, scriptPath, routeErr, params, found := s.luaRouter.GetRouteInfo(methodBytes, pathBytes) if found { if len(bytecode) == 0 || routeErr != nil { - errorMsg := "Route exists but failed to compile. Check server logs for details." - if routeErr != nil { - errorMsg = routeErr.Error() - } - logger.Error("%s %s - %s", method, path, errorMsg) - ctx.SetContentType("text/html; charset=utf-8") - ctx.SetStatusCode(fasthttp.StatusInternalServerError) - ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, errorMsg))) + s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, routeErr) } else { - logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count) - s.handleLuaRoute(ctx, bytecode, scriptPath, params, method, path) - } - } else if s.staticRouter != nil { - if _, found := s.staticRouter.Match(path); found { - s.staticRouter.ServeHTTP(ctx) - } else { - ctx.SetContentType("text/html; charset=utf-8") - ctx.SetStatusCode(fasthttp.StatusNotFound) - ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path))) + s.handleLuaRoute(ctx, bytecode, scriptPath, params, methodBytes, pathBytes) } } else { - ctx.SetContentType("text/html; charset=utf-8") - ctx.SetStatusCode(fasthttp.StatusNotFound) - ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path))) + s.send404(ctx, pathBytes) } if s.loggingEnabled { - logger.LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start)) + logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start)) } } -func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method, path string) { +func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, + params *router.Params, methodBytes, pathBytes []byte) { + luaCtx := runner.NewHTTPContext(ctx) defer luaCtx.Release() @@ -142,28 +196,47 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip session := s.sessionManager.GetSessionFromRequest(ctx) sessionMap["id"] = session.ID - sessionMap["data"] = session.GetAll() - luaCtx.Set("method", method) - luaCtx.Set("path", path) + // Only get session data if not empty + if !session.IsEmpty() { + sessionMap["data"] = session.GetAll() + } else { + sessionMap["data"] = emptyMap + } + + // Set basic context + luaCtx.Set("method", string(methodBytes)) + luaCtx.Set("path", string(pathBytes)) luaCtx.Set("host", string(ctx.Host())) luaCtx.Set("session", sessionMap) - if params.Count > 0 { - paramMap := make(map[string]any, params.Count) + // Handle params + if params != nil && params.Count > 0 { + paramMap := s.paramsPool.Get().(map[string]any) for i := 0; i < params.Count; i++ { paramMap[params.Keys[i]] = params.Values[i] } luaCtx.Set("params", paramMap) + defer func() { + for k := range paramMap { + delete(paramMap, k) + } + s.paramsPool.Put(paramMap) + }() } else { luaCtx.Set("params", emptyMap) } - if method == "POST" || method == "PUT" || method == "PATCH" { + // Parse form data for POST/PUT/PATCH + if bytes.Equal(methodBytes, methodPOST) || + bytes.Equal(methodBytes, methodPUT) || + bytes.Equal(methodBytes, methodPATCH) { if formData, err := ParseForm(ctx); err == nil { luaCtx.Set("form", formData) } else { - logger.Warning("Error parsing form: %v", err) + if s.debugMode { + logger.Warning("Error parsing form: %v", err) + } luaCtx.Set("form", emptyMap) } } else { @@ -172,23 +245,24 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath) if err != nil { - logger.Error("Error executing Lua route: %v", err) - ctx.SetContentType("text/html; charset=utf-8") - ctx.SetStatusCode(fasthttp.StatusInternalServerError) - ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, err.Error()))) + logger.Error("Lua execution error: %v", err) + s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, err) return } - if _, clearAll := response.SessionData["__clear_all"]; clearAll { - session.Clear() - delete(response.SessionData, "__clear_all") - } + // Handle session updates + if len(response.SessionData) > 0 { + if _, clearAll := response.SessionData["__clear_all"]; clearAll { + session.Clear() + delete(response.SessionData, "__clear_all") + } - for k, v := range response.SessionData { - if v == "__SESSION_DELETE_MARKER__" { - session.Delete(k) - } else { - session.Set(k, v) + for k, v := range response.SessionData { + if v == "__SESSION_DELETE_MARKER__" { + session.Delete(k) + } else { + session.Set(k, v) + } } } @@ -197,6 +271,33 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip runner.ReleaseResponse(response) } +func (s *Server) send404(ctx *fasthttp.RequestCtx, pathBytes []byte) { + ctx.SetContentType("text/html; charset=utf-8") + ctx.SetStatusCode(fasthttp.StatusNotFound) + + // Use cached 404 for common case + if len(pathBytes) == 1 && pathBytes[0] == '/' { + s.errorCacheMu.RLock() + ctx.SetBody(s.cached404) + s.errorCacheMu.RUnlock() + } else { + ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, string(pathBytes)))) + } +} + +func (s *Server) sendError(ctx *fasthttp.RequestCtx, status int, pathBytes []byte, err error) { + ctx.SetContentType("text/html; charset=utf-8") + ctx.SetStatusCode(status) + + if err == nil { + s.errorCacheMu.RLock() + ctx.SetBody(s.cached500) + s.errorCacheMu.RUnlock() + } else { + ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, string(pathBytes), err.Error()))) + } +} + func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) { stats := utils.CollectSystemStats(s.config) routeCount, bytecodeBytes := s.luaRouter.GetRouteStats() @@ -209,3 +310,24 @@ func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) { ctx.SetStatusCode(fasthttp.StatusOK) ctx.SetBody([]byte(utils.DebugStatsPage(stats))) } + +// SetStaticCaching enables/disables static file caching +func (s *Server) SetStaticCaching(duration time.Duration) { + if s.staticFS != nil { + s.staticFS.CacheDuration = duration + s.staticHandler = s.staticFS.NewRequestHandler() + } +} + +// GetStaticPrefix returns the URL prefix for static files +func (s *Server) GetStaticPrefix() string { + return s.staticPrefix +} + +// UpdateErrorCache refreshes cached error pages +func (s *Server) UpdateErrorCache() { + s.errorCacheMu.Lock() + s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, "")) + s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error")) + s.errorCacheMu.Unlock() +} diff --git a/main.go b/main.go index 1e2c1fa..5f6e18e 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( "time" "Moonshark/http" - "Moonshark/routers" + "Moonshark/router" "Moonshark/runner" "Moonshark/sessions" "Moonshark/utils/color" @@ -26,8 +26,7 @@ import ( // Moonshark represents the server and all its dependencies type Moonshark struct { Config *config.Config - LuaRouter *routers.LuaRouter - StaticRouter *routers.StaticRouter + LuaRouter *router.LuaRouter LuaRunner *runner.Runner HTTPServer *http.Server cleanupFuncs []func() error @@ -143,7 +142,7 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) { cfg.Server.Debug = true } - if err := initRouters(moonshark); err != nil { + if err := initLuaRouter(moonshark); err != nil { return nil, err } @@ -155,9 +154,18 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) { logger.Warning("Watcher setup failed: %v", err) } + // Get static directory - empty string if it doesn't exist + staticDir := "" + if dirExists(cfg.Dirs.Static) { + staticDir = cfg.Dirs.Static + logger.Info("Static files enabled: %s", color.Apply(staticDir, color.Yellow)) + } else { + logger.Warning("Static directory not found: %s", color.Apply(cfg.Dirs.Static, color.Yellow)) + } + moonshark.HTTPServer = http.New( moonshark.LuaRouter, - moonshark.StaticRouter, + staticDir, moonshark.LuaRunner, cfg.Server.HTTPLogging, cfg.Server.Debug, @@ -165,6 +173,13 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) { cfg, ) + // For development, disable caching. For production, enable it + if cfg.Server.Debug { + moonshark.HTTPServer.SetStaticCaching(0) // No caching in debug mode + } else { + moonshark.HTTPServer.SetStaticCaching(1 * time.Hour) // Cache for 1 hour in production + } + return moonshark, nil } @@ -249,15 +264,15 @@ func dirExists(path string) bool { return info.IsDir() } -func initRouters(s *Moonshark) error { +func initLuaRouter(s *Moonshark) error { if !dirExists(s.Config.Dirs.Routes) { return fmt.Errorf("routes directory doesn't exist: %s", s.Config.Dirs.Routes) } var err error - s.LuaRouter, err = routers.NewLuaRouter(s.Config.Dirs.Routes) + s.LuaRouter, err = router.NewLuaRouter(s.Config.Dirs.Routes) if err != nil { - if errors.Is(err, routers.ErrRoutesCompilationErrors) { + if errors.Is(err, router.ErrRoutesCompilationErrors) { // Non-fatal, some routes failed logger.Warning("Some routes failed to compile") @@ -272,16 +287,6 @@ func initRouters(s *Moonshark) error { } logger.Info("LuaRouter is g2g! %s", color.Set(s.Config.Dirs.Routes, color.Yellow)) - if dirExists(s.Config.Dirs.Static) { - s.StaticRouter, err = routers.NewStaticRouter(s.Config.Dirs.Static) - if err != nil { - return fmt.Errorf("static router init failed: %v", err) - } - logger.Info("StaticRouter is g2g! %s", color.Apply(s.Config.Dirs.Static, color.Yellow)) - } else { - logger.Warning("Static directory not found... %s", color.Apply(s.Config.Dirs.Static, color.Yellow)) - } - return nil } diff --git a/router/build.go b/router/build.go new file mode 100644 index 0000000..8f75dbd --- /dev/null +++ b/router/build.go @@ -0,0 +1,90 @@ +package router + +import ( + "os" + "path/filepath" + "strings" +) + +// buildRoutes scans the routes directory and builds the routing tree +func (r *LuaRouter) buildRoutes() error { + r.failedRoutes = make(map[string]*RouteError) + r.middlewareFiles = make(map[string][]string) + + // First pass: collect all middleware files + err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { + return err + } + + if strings.TrimSuffix(info.Name(), ".lua") == "middleware" { + relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) + if err != nil { + return err + } + + fsPath := "/" + if relDir != "." { + fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/") + } + + // Use filesystem path for middleware (includes groups) + r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path) + } + + return nil + }) + + if err != nil { + return err + } + + // Second pass: build routes with combined middleware + handler + return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { + return err + } + + fileName := strings.TrimSuffix(info.Name(), ".lua") + + // Skip middleware files (already processed) + if fileName == "middleware" { + return nil + } + + relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) + if err != nil { + return err + } + + fsPath := "/" + if relDir != "." { + fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/") + } + + pathInfo := parsePathWithGroups(fsPath) + + // Handle index.lua files + if fileName == "index" { + for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} { + root := r.routes[method] + node := r.findOrCreateNode(root, pathInfo.urlPath) + node.indexFile = path + node.modTime = info.ModTime() + node.fsPath = pathInfo.fsPath + r.compileWithMiddleware(node, pathInfo.fsPath, path) + } + return nil + } + + // Handle method files + method := strings.ToUpper(fileName) + root, exists := r.routes[method] + if !exists { + return nil + } + + r.addRoute(root, pathInfo, path, info.ModTime()) + return nil + }) +} diff --git a/router/cache.go b/router/cache.go new file mode 100644 index 0000000..6bc8cc5 --- /dev/null +++ b/router/cache.go @@ -0,0 +1,62 @@ +package router + +import ( + "encoding/binary" + "hash/fnv" + "time" + + "github.com/VictoriaMetrics/fastcache" +) + +// hashString generates a hash for a string +func hashString(s string) uint64 { + h := fnv.New64a() + h.Write([]byte(s)) + return h.Sum64() +} + +// uint64ToBytes converts a uint64 to bytes for cache key +func uint64ToBytes(n uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, n) + return b +} + +// getCacheKey generates a cache key for a method and path +func getCacheKey(method, path string) []byte { + key := hashString(method + ":" + path) + return uint64ToBytes(key) +} + +// getBytecodeKey generates a cache key for a handler path +func getBytecodeKey(handlerPath string) []byte { + key := hashString(handlerPath) + return uint64ToBytes(key) +} + +// ClearCache clears all caches +func (r *LuaRouter) ClearCache() { + r.routeCache.Reset() + r.bytecodeCache.Reset() + r.middlewareCache = make(map[string][]byte) + r.sourceCache = make(map[string][]byte) + r.sourceMtimes = make(map[string]time.Time) +} + +// GetCacheStats returns statistics about the cache +func (r *LuaRouter) GetCacheStats() map[string]any { + var routeStats fastcache.Stats + var bytecodeStats fastcache.Stats + + r.routeCache.UpdateStats(&routeStats) + r.bytecodeCache.UpdateStats(&bytecodeStats) + + return map[string]any{ + "routeEntries": routeStats.EntriesCount, + "routeBytes": routeStats.BytesSize, + "routeCollisions": routeStats.Collisions, + "bytecodeEntries": bytecodeStats.EntriesCount, + "bytecodeBytes": bytecodeStats.BytesSize, + "bytecodeCollisions": bytecodeStats.Collisions, + } +} diff --git a/router/compile.go b/router/compile.go new file mode 100644 index 0000000..ef83b21 --- /dev/null +++ b/router/compile.go @@ -0,0 +1,176 @@ +package router + +import ( + "os" + "strings" + "time" +) + +// compileWithMiddleware combines middleware and handler source, then compiles +func (r *LuaRouter) compileWithMiddleware(n *node, fsPath, scriptPath string) error { + if scriptPath == "" { + return nil + } + + // Check if we need to recompile by comparing modification times + sourceKey := r.getSourceCacheKey(fsPath, scriptPath) + needsRecompile := false + + // Check handler modification time + handlerInfo, err := os.Stat(scriptPath) + if err != nil { + n.err = err + return err + } + + lastCompiled, exists := r.sourceMtimes[sourceKey] + if !exists || handlerInfo.ModTime().After(lastCompiled) { + needsRecompile = true + } + + // Check middleware modification times + if !needsRecompile { + middlewareChain := r.getMiddlewareChain(fsPath) + for _, mwPath := range middlewareChain { + mwInfo, err := os.Stat(mwPath) + if err != nil { + n.err = err + return err + } + if mwInfo.ModTime().After(lastCompiled) { + needsRecompile = true + break + } + } + } + + // Use cached bytecode if available and fresh + if !needsRecompile { + if bytecode, exists := r.sourceCache[sourceKey]; exists { + bytecodeKey := getBytecodeKey(scriptPath) + r.bytecodeCache.Set(bytecodeKey, bytecode) + return nil + } + } + + // Build combined source + combinedSource, err := r.buildCombinedSource(fsPath, scriptPath) + if err != nil { + n.err = err + return err + } + + // Compile combined source using shared state + r.compileStateMu.Lock() + bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath) + r.compileStateMu.Unlock() + + if err != nil { + n.err = err + return err + } + + // Cache everything + bytecodeKey := getBytecodeKey(scriptPath) + r.bytecodeCache.Set(bytecodeKey, bytecode) + r.sourceCache[sourceKey] = bytecode + r.sourceMtimes[sourceKey] = time.Now() + + n.err = nil + return nil +} + +// buildCombinedSource builds the combined middleware + handler source +func (r *LuaRouter) buildCombinedSource(fsPath, scriptPath string) (string, error) { + var combinedSource strings.Builder + + // Get middleware chain using filesystem path + middlewareChain := r.getMiddlewareChain(fsPath) + + // Add middleware in order + for _, mwPath := range middlewareChain { + content, err := r.getFileContent(mwPath) + if err != nil { + return "", err + } + combinedSource.WriteString("-- Middleware: ") + combinedSource.WriteString(mwPath) + combinedSource.WriteString("\n") + combinedSource.Write(content) + combinedSource.WriteString("\n") + } + + // Add main handler + content, err := r.getFileContent(scriptPath) + if err != nil { + return "", err + } + combinedSource.WriteString("-- Handler: ") + combinedSource.WriteString(scriptPath) + combinedSource.WriteString("\n") + combinedSource.Write(content) + + return combinedSource.String(), nil +} + +// getFileContent reads file content with caching +func (r *LuaRouter) getFileContent(path string) ([]byte, error) { + // Check cache first + if content, exists := r.middlewareCache[path]; exists { + // Verify file hasn't changed + info, err := os.Stat(path) + if err == nil { + if cachedTime, exists := r.sourceMtimes[path]; exists && !info.ModTime().After(cachedTime) { + return content, nil + } + } + } + + // Read from disk + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + // Cache it + r.middlewareCache[path] = content + r.sourceMtimes[path] = time.Now() + + return content, nil +} + +// getSourceCacheKey generates a unique key for combined source +func (r *LuaRouter) getSourceCacheKey(fsPath, scriptPath string) string { + middlewareChain := r.getMiddlewareChain(fsPath) + var keyParts []string + keyParts = append(keyParts, middlewareChain...) + keyParts = append(keyParts, scriptPath) + return strings.Join(keyParts, "|") +} + +// getMiddlewareChain returns middleware files that apply to the given filesystem path +func (r *LuaRouter) getMiddlewareChain(fsPath string) []string { + var chain []string + + // Collect middleware from root to specific path using filesystem path (includes groups) + pathParts := strings.Split(strings.Trim(fsPath, "/"), "/") + if pathParts[0] == "" { + pathParts = []string{} + } + + // Add root middleware + if mw, exists := r.middlewareFiles["/"]; exists { + chain = append(chain, mw...) + } + + // Add middleware from each path level (including groups) + currentPath := "" + for _, part := range pathParts { + currentPath += "/" + part + if mw, exists := r.middlewareFiles[currentPath]; exists { + chain = append(chain, mw...) + } + } + + return chain +} diff --git a/routers/errors.go b/router/errors.go similarity index 97% rename from routers/errors.go rename to router/errors.go index 1b4405b..914c3b4 100644 --- a/routers/errors.go +++ b/router/errors.go @@ -1,4 +1,4 @@ -package routers +package router import "errors" diff --git a/router/match.go b/router/match.go new file mode 100644 index 0000000..c55ff6f --- /dev/null +++ b/router/match.go @@ -0,0 +1,187 @@ +package router + +import ( + "os" + "strings" +) + +// Match finds a handler for the given method and path (URL path, excludes groups) +func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) { + params.Count = 0 + + r.mu.RLock() + root, exists := r.routes[method] + r.mu.RUnlock() + + if !exists { + return nil, false + } + + segments := strings.Split(strings.Trim(path, "/"), "/") + return r.matchPath(root, segments, params, 0) +} + +// matchPath recursively matches a path against the routing tree +func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, depth int) (*node, bool) { + // Filter empty segments + filteredSegments := segments[:0] + for _, segment := range segments { + if segment != "" { + filteredSegments = append(filteredSegments, segment) + } + } + segments = filteredSegments + + if len(segments) == 0 { + if current.handler != "" { + return current, true + } + if current.indexFile != "" { + return current, true + } + return nil, false + } + + segment := segments[0] + remaining := segments[1:] + + // Try static child first + if child, exists := current.staticChild[segment]; exists { + if node, found := r.matchPath(child, remaining, params, depth+1); found { + return node, true + } + } + + // Try parameter child + if current.paramChild != nil { + if params.Count < maxParams { + params.Keys[params.Count] = current.paramChild.paramName + params.Values[params.Count] = segment + params.Count++ + } + + if node, found := r.matchPath(current.paramChild, remaining, params, depth+1); found { + return node, true + } + + params.Count-- + } + + // Fall back to index.lua + if current.indexFile != "" { + return current, true + } + + return nil, false +} + +// GetRouteInfo returns bytecode, script path, error, params, and found status +func (r *LuaRouter) GetRouteInfo(method, path []byte) ([]byte, string, error, *Params, bool) { + // Convert to string for internal processing + methodStr := string(method) + pathStr := string(path) + + routeCacheKey := getCacheKey(methodStr, pathStr) + routeCacheData := r.routeCache.Get(nil, routeCacheKey) + + // Fast path: found in cache + if len(routeCacheData) > 0 { + handlerPath := string(routeCacheData[8:]) + bytecodeKey := routeCacheData[:8] + + bytecode := r.bytecodeCache.Get(nil, bytecodeKey) + + n, exists := r.nodeForHandler(handlerPath) + if !exists { + r.routeCache.Del(routeCacheKey) + return nil, "", nil, nil, false + } + + // Check if recompilation needed + if len(bytecode) > 0 { + // For cached routes, we need to re-match to get params + params := &Params{} + r.Match(methodStr, pathStr, params) + return bytecode, handlerPath, n.err, params, true + } + + // Recompile if needed + fileInfo, err := os.Stat(handlerPath) + if err != nil || fileInfo.ModTime().After(n.modTime) { + scriptPath := n.handler + if scriptPath == "" { + scriptPath = n.indexFile + } + + fsPath := n.fsPath + if fsPath == "" { + fsPath = "/" + } + + if err := r.compileWithMiddleware(n, fsPath, scriptPath); err != nil { + params := &Params{} + r.Match(methodStr, pathStr, params) + return nil, handlerPath, n.err, params, true + } + + newBytecodeKey := getBytecodeKey(handlerPath) + bytecode = r.bytecodeCache.Get(nil, newBytecodeKey) + + newCacheData := make([]byte, 8+len(handlerPath)) + copy(newCacheData[:8], newBytecodeKey) + copy(newCacheData[8:], handlerPath) + r.routeCache.Set(routeCacheKey, newCacheData) + + params := &Params{} + r.Match(methodStr, pathStr, params) + return bytecode, handlerPath, n.err, params, true + } + + params := &Params{} + r.Match(methodStr, pathStr, params) + return bytecode, handlerPath, n.err, params, true + } + + // Slow path: lookup and compile + params := &Params{} + node, found := r.Match(methodStr, pathStr, params) + if !found { + return nil, "", nil, nil, false + } + + scriptPath := node.handler + if scriptPath == "" && node.indexFile != "" { + scriptPath = node.indexFile + } + + if scriptPath == "" { + return nil, "", nil, nil, false + } + + bytecodeKey := getBytecodeKey(scriptPath) + bytecode := r.bytecodeCache.Get(nil, bytecodeKey) + + if len(bytecode) == 0 { + fsPath := node.fsPath + if fsPath == "" { + fsPath = "/" + } + if err := r.compileWithMiddleware(node, fsPath, scriptPath); err != nil { + return nil, scriptPath, node.err, params, true + } + bytecode = r.bytecodeCache.Get(nil, bytecodeKey) + } + + // Cache the route + cacheData := make([]byte, 8+len(scriptPath)) + copy(cacheData[:8], bytecodeKey) + copy(cacheData[8:], scriptPath) + r.routeCache.Set(routeCacheKey, cacheData) + + return bytecode, scriptPath, node.err, params, true +} + +// GetRouteInfoString is a convenience method that accepts strings +func (r *LuaRouter) GetRouteInfoString(method, path string) ([]byte, string, error, *Params, bool) { + return r.GetRouteInfo([]byte(method), []byte(path)) +} diff --git a/router/node.go b/router/node.go new file mode 100644 index 0000000..01af455 --- /dev/null +++ b/router/node.go @@ -0,0 +1,190 @@ +package router + +import ( + "path/filepath" + "strings" + "time" +) + +// node represents a node in the radix trie +type node struct { + // Static children mapped by path segment + staticChild map[string]*node + + // Parameter child for dynamic segments (e.g., :id) + paramChild *node + paramName string + + // Handler information + handler string // Path to the handler file + indexFile string // Path to index.lua if exists + modTime time.Time // Modification time of the handler + fsPath string // Filesystem path (includes groups) + + // Compilation error if any + err error +} + +// pathInfo holds both URL path and filesystem path +type pathInfo struct { + urlPath string // URL path without groups (e.g., /users) + fsPath string // Filesystem path with groups (e.g., /(admin)/users) +} + +// parsePathWithGroups parses a filesystem path, extracting groups +func parsePathWithGroups(fsPath string) *pathInfo { + segments := strings.Split(strings.Trim(fsPath, "/"), "/") + var urlSegments []string + + for _, segment := range segments { + if segment == "" { + continue + } + + // Skip group segments (enclosed in parentheses) + if strings.HasPrefix(segment, "(") && strings.HasSuffix(segment, ")") { + continue + } + + urlSegments = append(urlSegments, segment) + } + + urlPath := "/" + if len(urlSegments) > 0 { + urlPath = "/" + strings.Join(urlSegments, "/") + } + + return &pathInfo{ + urlPath: urlPath, + fsPath: fsPath, + } +} + +// findOrCreateNode finds or creates a node at the given URL path +func (r *LuaRouter) findOrCreateNode(root *node, urlPath string) *node { + segments := strings.Split(strings.Trim(urlPath, "/"), "/") + if len(segments) == 1 && segments[0] == "" { + return root + } + + current := root + for _, segment := range segments { + if segment == "" { + continue + } + + // Check if it's a parameter + if strings.HasPrefix(segment, ":") { + paramName := segment[1:] + if current.paramChild == nil { + current.paramChild = &node{ + staticChild: make(map[string]*node), + paramName: paramName, + } + } + current = current.paramChild + } else { + // Static segment + if _, exists := current.staticChild[segment]; !exists { + current.staticChild[segment] = &node{ + staticChild: make(map[string]*node), + } + } + current = current.staticChild[segment] + } + } + + return current +} + +// addRoute adds a route to the tree +func (r *LuaRouter) addRoute(root *node, pathInfo *pathInfo, handlerPath string, modTime time.Time) { + node := r.findOrCreateNode(root, pathInfo.urlPath) + node.handler = handlerPath + node.modTime = modTime + node.fsPath = pathInfo.fsPath + + // Compile the route with middleware + r.compileWithMiddleware(node, pathInfo.fsPath, handlerPath) + + // Track failed routes + if node.err != nil { + key := filepath.Base(handlerPath) + ":" + pathInfo.urlPath + r.failedRoutes[key] = &RouteError{ + Path: pathInfo.urlPath, + Method: strings.ToUpper(strings.TrimSuffix(filepath.Base(handlerPath), ".lua")), + ScriptPath: handlerPath, + Err: node.err, + } + } +} + +// nodeForHandler finds a node by its handler path +func (r *LuaRouter) nodeForHandler(handlerPath string) (*node, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + for _, root := range r.routes { + if node := findNodeByHandler(root, handlerPath); node != nil { + return node, true + } + } + + return nil, false +} + +// findNodeByHandler recursively searches for a node with the given handler path +func findNodeByHandler(n *node, handlerPath string) *node { + if n.handler == handlerPath || n.indexFile == handlerPath { + return n + } + + // Search static children + for _, child := range n.staticChild { + if found := findNodeByHandler(child, handlerPath); found != nil { + return found + } + } + + // Search param child + if n.paramChild != nil { + if found := findNodeByHandler(n.paramChild, handlerPath); found != nil { + return found + } + } + + return nil +} + +// countNodesAndBytecode counts nodes and bytecode size in the tree +func countNodesAndBytecode(n *node) (int, int64) { + if n == nil { + return 0, 0 + } + + count := 0 + bytes := int64(0) + + // Count this node if it has a handler + if n.handler != "" || n.indexFile != "" { + count = 1 + // Estimate bytecode size (would need actual bytecode cache lookup for accuracy) + bytes = 1024 // Placeholder + } + + // Count static children + for _, child := range n.staticChild { + c, b := countNodesAndBytecode(child) + count += c + bytes += b + } + + // Count param child + if n.paramChild != nil { + c, b := countNodesAndBytecode(n.paramChild) + count += c + bytes += b + } + + return count, bytes +} diff --git a/router/params.go b/router/params.go new file mode 100644 index 0000000..f32f4b4 --- /dev/null +++ b/router/params.go @@ -0,0 +1,44 @@ +package router + +// Maximum number of URL parameters per route +const maxParams = 20 + +// Params holds URL parameters with fixed-size arrays to avoid allocations +type Params struct { + Keys [maxParams]string + Values [maxParams]string + Count int +} + +// Get returns a parameter value by name +func (p *Params) Get(name string) string { + for i := range p.Count { + if p.Keys[i] == name { + return p.Values[i] + } + } + return "" +} + +// Reset clears all parameters +func (p *Params) Reset() { + p.Count = 0 +} + +// Set adds or updates a parameter +func (p *Params) Set(name, value string) { + // Try to update existing + for i := range p.Count { + if p.Keys[i] == name { + p.Values[i] = value + return + } + } + + // Add new if space available + if p.Count < maxParams { + p.Keys[p.Count] = name + p.Values[p.Count] = value + p.Count++ + } +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..df8429f --- /dev/null +++ b/router/router.go @@ -0,0 +1,156 @@ +package router + +import ( + "errors" + "os" + "sync" + "time" + + luajit "git.sharkk.net/Sky/LuaJIT-to-Go" + "github.com/VictoriaMetrics/fastcache" +) + +// Default cache sizes +const ( + defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache + defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache +) + +// LuaRouter is a filesystem-based HTTP router for Lua files +type LuaRouter struct { + routesDir string // Root directory containing route files + routes map[string]*node // Method -> route tree + failedRoutes map[string]*RouteError // Track failed routes + mu sync.RWMutex // Lock for concurrent access to routes + + routeCache *fastcache.Cache // Cache for route lookups + bytecodeCache *fastcache.Cache // Cache for compiled bytecode + + // Middleware tracking for path hierarchy + middlewareFiles map[string][]string // filesystem path -> middleware file paths + + // Caching fields + middlewareCache map[string][]byte // path -> content + sourceCache map[string][]byte // combined source cache key -> compiled bytecode + sourceMtimes map[string]time.Time // track modification times + + // Shared Lua state for compilation + compileState *luajit.State + compileStateMu sync.Mutex // Protect concurrent access to Lua state +} + +// NewLuaRouter creates a new LuaRouter instance +func NewLuaRouter(routesDir string) (*LuaRouter, error) { + info, err := os.Stat(routesDir) + if err != nil { + return nil, err + } + if !info.IsDir() { + return nil, errors.New("routes path is not a directory") + } + + // Create shared Lua state + compileState := luajit.New() + if compileState == nil { + return nil, errors.New("failed to create Lua compile state") + } + + r := &LuaRouter{ + routesDir: routesDir, + routes: make(map[string]*node), + failedRoutes: make(map[string]*RouteError), + middlewareFiles: make(map[string][]string), + routeCache: fastcache.New(defaultRouteMaxBytes), + bytecodeCache: fastcache.New(defaultBytecodeMaxBytes), + middlewareCache: make(map[string][]byte), + sourceCache: make(map[string][]byte), + sourceMtimes: make(map[string]time.Time), + compileState: compileState, + } + + methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} + for _, method := range methods { + r.routes[method] = &node{ + staticChild: make(map[string]*node), + } + } + + err = r.buildRoutes() + + if len(r.failedRoutes) > 0 { + return r, ErrRoutesCompilationErrors + } + + return r, err +} + +// Refresh rebuilds the router by rescanning the routes directory +func (r *LuaRouter) Refresh() error { + r.mu.Lock() + defer r.mu.Unlock() + + for method := range r.routes { + r.routes[method] = &node{ + staticChild: make(map[string]*node), + } + } + + r.failedRoutes = make(map[string]*RouteError) + r.middlewareFiles = make(map[string][]string) + r.middlewareCache = make(map[string][]byte) + r.sourceCache = make(map[string][]byte) + r.sourceMtimes = make(map[string]time.Time) + + err := r.buildRoutes() + + if len(r.failedRoutes) > 0 { + return ErrRoutesCompilationErrors + } + + return err +} + +// ReportFailedRoutes returns a list of routes that failed to compile +func (r *LuaRouter) ReportFailedRoutes() []*RouteError { + r.mu.RLock() + defer r.mu.RUnlock() + + result := make([]*RouteError, 0, len(r.failedRoutes)) + for _, re := range r.failedRoutes { + result = append(result, re) + } + + return result +} + +// Close cleans up the router and its resources +func (r *LuaRouter) Close() { + r.compileStateMu.Lock() + if r.compileState != nil { + r.compileState.Close() + r.compileState = nil + } + r.compileStateMu.Unlock() +} + +// GetRouteStats returns statistics about the router +func (r *LuaRouter) GetRouteStats() (int, int64) { + r.mu.RLock() + defer r.mu.RUnlock() + + routeCount := 0 + bytecodeBytes := int64(0) + + for _, root := range r.routes { + count, bytes := countNodesAndBytecode(root) + routeCount += count + bytecodeBytes += bytes + } + + return routeCount, bytecodeBytes +} + +type NodeWithError struct { + ScriptPath string + Error error +} diff --git a/routers/luaRouter.go b/routers/luaRouter.go deleted file mode 100644 index cd6de66..0000000 --- a/routers/luaRouter.go +++ /dev/null @@ -1,827 +0,0 @@ -package routers - -import ( - "encoding/binary" - "errors" - "hash/fnv" - "os" - "path/filepath" - "strings" - "sync" - "time" - - luajit "git.sharkk.net/Sky/LuaJIT-to-Go" - "github.com/VictoriaMetrics/fastcache" -) - -// Maximum number of URL parameters per route -const maxParams = 20 - -// Default cache sizes -const ( - defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache - defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache -) - -// Params holds URL parameters with fixed-size arrays to avoid allocations -type Params struct { - Keys [maxParams]string - Values [maxParams]string - Count int -} - -// Get returns a parameter value by name -func (p *Params) Get(name string) string { - for i := range p.Count { - if p.Keys[i] == name { - return p.Values[i] - } - } - return "" -} - -// LuaRouter is a filesystem-based HTTP router for Lua files -type LuaRouter struct { - routesDir string // Root directory containing route files - routes map[string]*node // Method -> route tree - failedRoutes map[string]*RouteError // Track failed routes - mu sync.RWMutex // Lock for concurrent access to routes - - routeCache *fastcache.Cache // Cache for route lookups - bytecodeCache *fastcache.Cache // Cache for compiled bytecode - - // Middleware tracking for path hierarchy - middlewareFiles map[string][]string // filesystem path -> middleware file paths - - // New caching fields - middlewareCache map[string][]byte // path -> content - sourceCache map[string][]byte // combined source cache key -> compiled bytecode - sourceMtimes map[string]time.Time // track modification times - - // Shared Lua state for compilation - compileState *luajit.State - compileStateMu sync.Mutex // Protect concurrent access to Lua state -} - -// node represents a node in the routing trie -type node struct { - handler string // Path to Lua file (empty if not an endpoint) - indexFile string // Path to index.lua file (catch-all) - paramName string // Parameter name (if this is a parameter node) - staticChild map[string]*node // Static children by segment name - paramChild *node // Parameter/wildcard child - err error // Compilation error if any - modTime time.Time // Last modification time - fsPath string // Original filesystem path (includes groups) -} - -// pathInfo holds both filesystem and URL path information -type pathInfo struct { - fsPath string // Filesystem path (includes groups) - urlPath string // URL path (excludes groups) -} - -// parsePathWithGroups separates filesystem path from URL path -func parsePathWithGroups(fsPath string) pathInfo { - segments := strings.Split(strings.Trim(fsPath, "/"), "/") - var urlSegments []string - - for _, segment := range segments { - if segment == "" { - continue - } - // Skip group segments for URL path - if len(segment) >= 3 && segment[0] == '(' && segment[len(segment)-1] == ')' { - continue - } - urlSegments = append(urlSegments, segment) - } - - urlPath := "/" - if len(urlSegments) > 0 { - urlPath = "/" + strings.Join(urlSegments, "/") - } - - return pathInfo{ - fsPath: fsPath, - urlPath: urlPath, - } -} - -// NewLuaRouter creates a new LuaRouter instance -func NewLuaRouter(routesDir string) (*LuaRouter, error) { - info, err := os.Stat(routesDir) - if err != nil { - return nil, err - } - if !info.IsDir() { - return nil, errors.New("routes path is not a directory") - } - - // Create shared Lua state - compileState := luajit.New() - if compileState == nil { - return nil, errors.New("failed to create Lua compile state") - } - - r := &LuaRouter{ - routesDir: routesDir, - routes: make(map[string]*node), - failedRoutes: make(map[string]*RouteError), - middlewareFiles: make(map[string][]string), - routeCache: fastcache.New(defaultRouteMaxBytes), - bytecodeCache: fastcache.New(defaultBytecodeMaxBytes), - middlewareCache: make(map[string][]byte), - sourceCache: make(map[string][]byte), - sourceMtimes: make(map[string]time.Time), - compileState: compileState, - } - - methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} - for _, method := range methods { - r.routes[method] = &node{ - staticChild: make(map[string]*node), - } - } - - err = r.buildRoutes() - - if len(r.failedRoutes) > 0 { - return r, ErrRoutesCompilationErrors - } - - return r, err -} - -// buildRoutes scans the routes directory and builds the routing tree -func (r *LuaRouter) buildRoutes() error { - r.failedRoutes = make(map[string]*RouteError) - r.middlewareFiles = make(map[string][]string) - - // First pass: collect all middleware files - err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { - return err - } - - if strings.TrimSuffix(info.Name(), ".lua") == "middleware" { - relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) - if err != nil { - return err - } - - fsPath := "/" - if relDir != "." { - fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/") - } - - // Use filesystem path for middleware (includes groups) - r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path) - } - - return nil - }) - - if err != nil { - return err - } - - // Second pass: build routes with combined middleware + handler - return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { - return err - } - - fileName := strings.TrimSuffix(info.Name(), ".lua") - - // Skip middleware files (already processed) - if fileName == "middleware" { - return nil - } - - relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) - if err != nil { - return err - } - - fsPath := "/" - if relDir != "." { - fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/") - } - - pathInfo := parsePathWithGroups(fsPath) - - // Handle index.lua files - if fileName == "index" { - for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} { - root := r.routes[method] - node := r.findOrCreateNode(root, pathInfo.urlPath) - node.indexFile = path - node.modTime = info.ModTime() - node.fsPath = pathInfo.fsPath - r.compileWithMiddleware(node, pathInfo.fsPath, path) - } - return nil - } - - // Handle method files - method := strings.ToUpper(fileName) - root, exists := r.routes[method] - if !exists { - return nil - } - - r.addRoute(root, pathInfo, path, info.ModTime()) - return nil - }) -} - -// addRoute adds a route to the routing tree -func (r *LuaRouter) addRoute(root *node, pathInfo pathInfo, handlerPath string, modTime time.Time) error { - segments := strings.Split(strings.Trim(pathInfo.urlPath, "/"), "/") - current := root - - for _, segment := range segments { - if segment == "" { - continue - } - - if len(segment) >= 2 && segment[0] == '[' && segment[len(segment)-1] == ']' { - if current.paramChild == nil { - current.paramChild = &node{ - paramName: segment[1 : len(segment)-1], - staticChild: make(map[string]*node), - } - } - current = current.paramChild - } else { - child, exists := current.staticChild[segment] - if !exists { - child = &node{ - staticChild: make(map[string]*node), - } - current.staticChild[segment] = child - } - current = child - } - } - - current.handler = handlerPath - current.modTime = modTime - current.fsPath = pathInfo.fsPath - - return r.compileWithMiddleware(current, pathInfo.fsPath, handlerPath) -} - -// compileWithMiddleware combines middleware and handler source, then compiles -func (r *LuaRouter) compileWithMiddleware(n *node, fsPath, scriptPath string) error { - if scriptPath == "" { - return nil - } - - // Check if we need to recompile by comparing modification times - sourceKey := r.getSourceCacheKey(fsPath, scriptPath) - needsRecompile := false - - // Check handler modification time - handlerInfo, err := os.Stat(scriptPath) - if err != nil { - n.err = err - return err - } - - lastCompiled, exists := r.sourceMtimes[sourceKey] - if !exists || handlerInfo.ModTime().After(lastCompiled) { - needsRecompile = true - } - - // Check middleware modification times - if !needsRecompile { - middlewareChain := r.getMiddlewareChain(fsPath) - for _, mwPath := range middlewareChain { - mwInfo, err := os.Stat(mwPath) - if err != nil { - n.err = err - return err - } - if mwInfo.ModTime().After(lastCompiled) { - needsRecompile = true - break - } - } - } - - // Use cached bytecode if available and fresh - if !needsRecompile { - if bytecode, exists := r.sourceCache[sourceKey]; exists { - bytecodeKey := getBytecodeKey(scriptPath) - r.bytecodeCache.Set(bytecodeKey, bytecode) - return nil - } - } - - // Build combined source - combinedSource, err := r.buildCombinedSource(fsPath, scriptPath) - if err != nil { - n.err = err - return err - } - - // Compile combined source using shared state - r.compileStateMu.Lock() - bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath) - r.compileStateMu.Unlock() - - if err != nil { - n.err = err - return err - } - - // Cache everything - bytecodeKey := getBytecodeKey(scriptPath) - r.bytecodeCache.Set(bytecodeKey, bytecode) - r.sourceCache[sourceKey] = bytecode - r.sourceMtimes[sourceKey] = time.Now() - - n.err = nil - return nil -} - -// buildCombinedSource builds the combined middleware + handler source -func (r *LuaRouter) buildCombinedSource(fsPath, scriptPath string) (string, error) { - var combinedSource strings.Builder - - // Get middleware chain using filesystem path - middlewareChain := r.getMiddlewareChain(fsPath) - - // Add middleware in order - for _, mwPath := range middlewareChain { - content, err := r.getFileContent(mwPath) - if err != nil { - return "", err - } - combinedSource.WriteString("-- Middleware: ") - combinedSource.WriteString(mwPath) - combinedSource.WriteString("\n") - combinedSource.Write(content) - combinedSource.WriteString("\n") - } - - // Add main handler - content, err := r.getFileContent(scriptPath) - if err != nil { - return "", err - } - combinedSource.WriteString("-- Handler: ") - combinedSource.WriteString(scriptPath) - combinedSource.WriteString("\n") - combinedSource.Write(content) - - return combinedSource.String(), nil -} - -// getFileContent reads file content with caching -func (r *LuaRouter) getFileContent(path string) ([]byte, error) { - // Check cache first - if content, exists := r.middlewareCache[path]; exists { - // Verify file hasn't changed - info, err := os.Stat(path) - if err == nil { - if cachedTime, exists := r.sourceMtimes[path]; exists && !info.ModTime().After(cachedTime) { - return content, nil - } - } - } - - // Read from disk - content, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - // Cache it - r.middlewareCache[path] = content - r.sourceMtimes[path] = time.Now() - - return content, nil -} - -// getSourceCacheKey generates a unique key for combined source -func (r *LuaRouter) getSourceCacheKey(fsPath, scriptPath string) string { - middlewareChain := r.getMiddlewareChain(fsPath) - var keyParts []string - keyParts = append(keyParts, middlewareChain...) - keyParts = append(keyParts, scriptPath) - return strings.Join(keyParts, "|") -} - -// getMiddlewareChain returns middleware files that apply to the given filesystem path -func (r *LuaRouter) getMiddlewareChain(fsPath string) []string { - var chain []string - - // Collect middleware from root to specific path using filesystem path (includes groups) - pathParts := strings.Split(strings.Trim(fsPath, "/"), "/") - if pathParts[0] == "" { - pathParts = []string{} - } - - // Add root middleware - if mw, exists := r.middlewareFiles["/"]; exists { - chain = append(chain, mw...) - } - - // Add middleware from each path level (including groups) - currentPath := "" - for _, part := range pathParts { - currentPath += "/" + part - if mw, exists := r.middlewareFiles[currentPath]; exists { - chain = append(chain, mw...) - } - } - - return chain -} - -// findOrCreateNode finds or creates a node at the given URL path (excludes groups) -func (r *LuaRouter) findOrCreateNode(root *node, urlPath string) *node { - segments := strings.Split(strings.Trim(urlPath, "/"), "/") - current := root - - for _, segment := range segments { - if segment == "" { - continue - } - - if len(segment) >= 2 && segment[0] == '[' && segment[len(segment)-1] == ']' { - if current.paramChild == nil { - current.paramChild = &node{ - paramName: segment[1 : len(segment)-1], - staticChild: make(map[string]*node), - } - } - current = current.paramChild - } else { - child, exists := current.staticChild[segment] - if !exists { - child = &node{ - staticChild: make(map[string]*node), - } - current.staticChild[segment] = child - } - current = child - } - } - - return current -} - -// getRouteKey generates a unique key for a route -func getRouteKey(path, handler string) string { - return path + ":" + handler -} - -// hashString generates a hash for a string -func hashString(s string) uint64 { - h := fnv.New64a() - h.Write([]byte(s)) - return h.Sum64() -} - -// uint64ToBytes converts a uint64 to bytes for cache key -func uint64ToBytes(n uint64) []byte { - b := make([]byte, 8) - binary.LittleEndian.PutUint64(b, n) - return b -} - -// getCacheKey generates a cache key for a method and path -func getCacheKey(method, path string) []byte { - key := hashString(method + ":" + path) - return uint64ToBytes(key) -} - -// getBytecodeKey generates a cache key for a handler path -func getBytecodeKey(handlerPath string) []byte { - key := hashString(handlerPath) - return uint64ToBytes(key) -} - -// Match finds a handler for the given method and path (URL path, excludes groups) -func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) { - params.Count = 0 - - r.mu.RLock() - root, exists := r.routes[method] - r.mu.RUnlock() - - if !exists { - return nil, false - } - - segments := strings.Split(strings.Trim(path, "/"), "/") - return r.matchPath(root, segments, params, 0) -} - -// matchPath recursively matches a path against the routing tree -func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, depth int) (*node, bool) { - // Filter empty segments - filteredSegments := segments[:0] - for _, segment := range segments { - if segment != "" { - filteredSegments = append(filteredSegments, segment) - } - } - segments = filteredSegments - - if len(segments) == 0 { - if current.handler != "" { - return current, true - } - if current.indexFile != "" { - return current, true - } - return nil, false - } - - segment := segments[0] - remaining := segments[1:] - - // Try static child first - if child, exists := current.staticChild[segment]; exists { - if node, found := r.matchPath(child, remaining, params, depth+1); found { - return node, true - } - } - - // Try parameter child - if current.paramChild != nil { - if params.Count < maxParams { - params.Keys[params.Count] = current.paramChild.paramName - params.Values[params.Count] = segment - params.Count++ - } - - if node, found := r.matchPath(current.paramChild, remaining, params, depth+1); found { - return node, true - } - - params.Count-- - } - - // Fall back to index.lua - if current.indexFile != "" { - return current, true - } - - return nil, false -} - -// GetRouteInfo returns the combined bytecode, script path, and any error -func (r *LuaRouter) GetRouteInfo(method, path string, params *Params) ([]byte, string, error, bool) { - routeCacheKey := getCacheKey(method, path) - routeCacheData := r.routeCache.Get(nil, routeCacheKey) - - if len(routeCacheData) > 0 { - handlerPath := string(routeCacheData[8:]) - bytecodeKey := routeCacheData[:8] - - bytecode := r.bytecodeCache.Get(nil, bytecodeKey) - - n, exists := r.nodeForHandler(handlerPath) - if !exists { - r.routeCache.Del(routeCacheKey) - return nil, "", nil, false - } - - if len(bytecode) > 0 { - return bytecode, handlerPath, n.err, true - } - - fileInfo, err := os.Stat(handlerPath) - if err != nil || fileInfo.ModTime().After(n.modTime) { - scriptPath := n.handler - if scriptPath == "" { - scriptPath = n.indexFile - } - - fsPath := n.fsPath - if fsPath == "" { - fsPath = "/" - } - - if err := r.compileWithMiddleware(n, fsPath, scriptPath); err != nil { - return nil, handlerPath, n.err, true - } - - newBytecodeKey := getBytecodeKey(handlerPath) - bytecode = r.bytecodeCache.Get(nil, newBytecodeKey) - - newCacheData := make([]byte, 8+len(handlerPath)) - copy(newCacheData[:8], newBytecodeKey) - copy(newCacheData[8:], handlerPath) - r.routeCache.Set(routeCacheKey, newCacheData) - - return bytecode, handlerPath, n.err, true - } - - return bytecode, handlerPath, n.err, true - } - - node, found := r.Match(method, path, params) - if !found { - return nil, "", nil, false - } - - scriptPath := node.handler - if scriptPath == "" && node.indexFile != "" { - scriptPath = node.indexFile - } - - if scriptPath == "" { - return nil, "", nil, false - } - - bytecodeKey := getBytecodeKey(scriptPath) - bytecode := r.bytecodeCache.Get(nil, bytecodeKey) - - if len(bytecode) == 0 { - fsPath := node.fsPath - if fsPath == "" { - fsPath = "/" - } - if err := r.compileWithMiddleware(node, fsPath, scriptPath); err != nil { - return nil, scriptPath, node.err, true - } - bytecode = r.bytecodeCache.Get(nil, bytecodeKey) - } - - cacheData := make([]byte, 8+len(scriptPath)) - copy(cacheData[:8], bytecodeKey) - copy(cacheData[8:], scriptPath) - r.routeCache.Set(routeCacheKey, cacheData) - - return bytecode, scriptPath, node.err, true -} - -// nodeForHandler finds a node by its handler path -func (r *LuaRouter) nodeForHandler(handlerPath string) (*node, bool) { - r.mu.RLock() - defer r.mu.RUnlock() - - for _, root := range r.routes { - if node := findNodeByHandler(root, handlerPath); node != nil { - return node, true - } - } - - return nil, false -} - -// findNodeByHandler finds a node by its handler path -func findNodeByHandler(current *node, handlerPath string) *node { - if current == nil { - return nil - } - - if current.handler == handlerPath || current.indexFile == handlerPath { - return current - } - - for _, child := range current.staticChild { - if node := findNodeByHandler(child, handlerPath); node != nil { - return node - } - } - - if current.paramChild != nil { - if node := findNodeByHandler(current.paramChild, handlerPath); node != nil { - return node - } - } - - return nil -} - -// Refresh rebuilds the router by rescanning the routes directory -func (r *LuaRouter) Refresh() error { - r.mu.Lock() - defer r.mu.Unlock() - - for method := range r.routes { - r.routes[method] = &node{ - staticChild: make(map[string]*node), - } - } - - r.failedRoutes = make(map[string]*RouteError) - r.middlewareFiles = make(map[string][]string) - r.middlewareCache = make(map[string][]byte) - r.sourceCache = make(map[string][]byte) - r.sourceMtimes = make(map[string]time.Time) - - err := r.buildRoutes() - - if len(r.failedRoutes) > 0 { - return ErrRoutesCompilationErrors - } - - return err -} - -// ReportFailedRoutes returns a list of routes that failed to compile -func (r *LuaRouter) ReportFailedRoutes() []*RouteError { - r.mu.RLock() - defer r.mu.RUnlock() - - result := make([]*RouteError, 0, len(r.failedRoutes)) - for _, re := range r.failedRoutes { - result = append(result, re) - } - - return result -} - -// ClearCache clears all caches -func (r *LuaRouter) ClearCache() { - r.routeCache.Reset() - r.bytecodeCache.Reset() - r.middlewareCache = make(map[string][]byte) - r.sourceCache = make(map[string][]byte) - r.sourceMtimes = make(map[string]time.Time) -} - -// Close cleans up the router and its resources -func (r *LuaRouter) Close() { - r.compileStateMu.Lock() - if r.compileState != nil { - r.compileState.Close() - r.compileState = nil - } - r.compileStateMu.Unlock() -} - -// GetCacheStats returns statistics about the cache -func (r *LuaRouter) GetCacheStats() map[string]any { - var routeStats fastcache.Stats - var bytecodeStats fastcache.Stats - - r.routeCache.UpdateStats(&routeStats) - r.bytecodeCache.UpdateStats(&bytecodeStats) - - return map[string]any{ - "routeEntries": routeStats.EntriesCount, - "routeBytes": routeStats.BytesSize, - "routeCollisions": routeStats.Collisions, - "bytecodeEntries": bytecodeStats.EntriesCount, - "bytecodeBytes": bytecodeStats.BytesSize, - "bytecodeCollisions": bytecodeStats.Collisions, - } -} - -// GetRouteStats returns statistics about the router -func (r *LuaRouter) GetRouteStats() (int, int64) { - r.mu.RLock() - defer r.mu.RUnlock() - - routeCount := 0 - bytecodeBytes := int64(0) - - for _, root := range r.routes { - count, bytes := countNodesAndBytecode(root) - routeCount += count - bytecodeBytes += bytes - } - - return routeCount, bytecodeBytes -} - -// countNodesAndBytecode traverses the tree and counts nodes and bytecode size -func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) { - if n == nil { - return 0, 0 - } - - if n.handler != "" || n.indexFile != "" { - count = 1 - bytecodeBytes = 2048 - } - - for _, child := range n.staticChild { - childCount, childBytes := countNodesAndBytecode(child) - count += childCount - bytecodeBytes += childBytes - } - - if n.paramChild != nil { - childCount, childBytes := countNodesAndBytecode(n.paramChild) - count += childCount - bytecodeBytes += childBytes - } - - return count, bytecodeBytes -} - -type NodeWithError struct { - ScriptPath string - Error error -} diff --git a/routers/staticRouter.go b/routers/staticRouter.go deleted file mode 100644 index 2075b5b..0000000 --- a/routers/staticRouter.go +++ /dev/null @@ -1,160 +0,0 @@ -package routers - -import ( - "errors" - "io/fs" - "os" - "path/filepath" - "strings" - "time" - - "Moonshark/utils/logger" - - "github.com/valyala/fasthttp" -) - -// StaticRouter is a simplified router for static files using FastHTTP's built-in capabilities -type StaticRouter struct { - fs *fasthttp.FS - fsHandler fasthttp.RequestHandler - urlPrefix string - rootDir string - log bool - useBrotli bool - useZstd bool -} - -// NewStaticRouter creates a new StaticRouter instance -func NewStaticRouter(rootDir string) (*StaticRouter, error) { - info, err := os.Stat(rootDir) - if err != nil { - return nil, err - } - if !info.IsDir() { - return nil, errors.New("root path is not a directory") - } - - fs := &fasthttp.FS{ - Root: rootDir, - IndexNames: []string{"index.html"}, - GenerateIndexPages: false, - AcceptByteRange: true, - Compress: true, - CacheDuration: 24 * time.Hour, - CompressedFileSuffix: ".gz", - CompressBrotli: true, - CompressZstd: true, - } - - r := &StaticRouter{ - fs: fs, - urlPrefix: "", - rootDir: rootDir, - log: false, - useBrotli: true, - useZstd: true, - } - - r.updatePathRewrite() - - return r, nil -} - -// WithEmbeddedFS sets an embedded filesystem instead of using the rootDir -func (r *StaticRouter) WithEmbeddedFS(embedded fs.FS) *StaticRouter { - r.fs.FS = embedded - r.fsHandler = r.fs.NewRequestHandler() - return r -} - -// SetCompression configures the compression options -func (r *StaticRouter) SetCompression(useGzip, useBrotli, useZstd bool) { - r.fs.Compress = useGzip - r.fs.CompressBrotli = useBrotli - r.fs.CompressZstd = useZstd - r.useBrotli = useBrotli - r.useZstd = useZstd - - r.fsHandler = r.fs.NewRequestHandler() -} - -// SetCacheDuration sets the cache duration for HTTP headers -func (r *StaticRouter) SetCacheDuration(duration time.Duration) { - r.fs.CacheDuration = duration - r.fsHandler = r.fs.NewRequestHandler() -} - -// SetURLPrefix sets the URL prefix for static assets -func (r *StaticRouter) SetURLPrefix(prefix string) { - if !strings.HasPrefix(prefix, "/") { - prefix = "/" + prefix - } - r.urlPrefix = prefix - r.updatePathRewrite() -} - -// updatePathRewrite updates the path rewriter based on the current prefix -func (r *StaticRouter) updatePathRewrite() { - r.fs.PathRewrite = fasthttp.NewPathPrefixStripper(len(r.urlPrefix)) - r.fsHandler = r.fs.NewRequestHandler() -} - -// EnableDebugLog enables debug logging -func (r *StaticRouter) EnableDebugLog() { - r.log = true -} - -// DisableDebugLog disables debug logging -func (r *StaticRouter) DisableDebugLog() { - r.log = false -} - -// ServeHTTP implements the http.Handler interface through fasthttpadaptor -func (r *StaticRouter) ServeHTTP(ctx *fasthttp.RequestCtx) { - path := string(ctx.Path()) - - if !strings.HasPrefix(path, r.urlPrefix) { - ctx.NotFound() - return - } - - if r.log { - logger.Debug("[StaticRouter] Serving: %s", path) - } - - r.fsHandler(ctx) -} - -// Match finds a file path for the given URL path -func (r *StaticRouter) Match(urlPath string) (string, bool) { - if !strings.HasPrefix(urlPath, r.urlPrefix) { - return "", false - } - - urlPath = strings.TrimPrefix(urlPath, r.urlPrefix) - - if !strings.HasPrefix(urlPath, "/") { - urlPath = "/" + urlPath - } - - filePath := filepath.Join(r.rootDir, filepath.FromSlash(strings.TrimPrefix(urlPath, "/"))) - _, err := os.Stat(filePath) - - if r.log && err == nil { - logger.Debug("[StaticRouter] MATCH: %s -> %s", urlPath, filePath) - } - - return filePath, err == nil -} - -// GetStats returns basic stats about the router -func (r *StaticRouter) GetStats() map[string]any { - return map[string]any{ - "type": "StaticRouter", - "rootDir": r.rootDir, - "urlPrefix": r.urlPrefix, - "useBrotli": r.useBrotli, - "useZstd": r.useZstd, - "cacheTime": r.fs.CacheDuration.String(), - } -} diff --git a/sessions/session.go b/sessions/session.go index 5c11823..28c8a47 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -427,3 +427,8 @@ func deepEqual(a, b any) bool { return false } + +// IsEmpty returns true if the session has no data +func (s *Session) IsEmpty() bool { + return len(s.Data) == 0 +} diff --git a/tests/basic_test.go b/tests/basic_test.go deleted file mode 100644 index 38c446b..0000000 --- a/tests/basic_test.go +++ /dev/null @@ -1,353 +0,0 @@ -package tests - -import ( - "context" - "log" - "os" - "path/filepath" - "testing" - - "Moonshark/routers" - "Moonshark/runner" - "Moonshark/utils/logger" -) - -// setupTestEnv initializes test components and returns cleanup function -func setupTestEnv(b *testing.B) (*routers.LuaRouter, *runner.Runner, func()) { - // Completely silence all logging - originalStderr := os.Stderr - originalStdout := os.Stdout - devNull, _ := os.Open(os.DevNull) - - // Redirect everything to devnull - os.Stderr = devNull - os.Stdout = devNull - log.SetOutput(devNull) - - // Force reinit logger to be silent - logger.InitGlobalLogger(false, false) - - // Create temp directories - tempDir, err := os.MkdirTemp("", "moonshark-bench") - if err != nil { - b.Fatalf("Failed to create temp dir: %v", err) - } - - routesDir := filepath.Join(tempDir, "routes") - staticDir := filepath.Join(tempDir, "static") - libsDir := filepath.Join(tempDir, "libs") - dataDir := filepath.Join(tempDir, "data") - fsDir := filepath.Join(tempDir, "fs") - - os.MkdirAll(routesDir, 0755) - os.MkdirAll(staticDir, 0755) - os.MkdirAll(libsDir, 0755) - os.MkdirAll(dataDir, 0755) - os.MkdirAll(fsDir, 0755) - - // Create test routes - createTestRoutes(routesDir) - - // Initialize router - luaRouter, err := routers.NewLuaRouter(routesDir) - if err != nil { - b.Fatalf("Failed to create router: %v", err) - } - - // Initialize runner - luaRunner, err := runner.NewRunner( - runner.WithPoolSize(4), - runner.WithLibDirs(libsDir), - runner.WithDataDir(dataDir), - runner.WithFsDir(fsDir), - ) - if err != nil { - b.Fatalf("Failed to create runner: %v", err) - } - - // Return cleanup function that restores outputs - cleanup := func() { - luaRunner.Close() - os.RemoveAll(tempDir) - os.Stderr = originalStderr - os.Stdout = originalStdout - devNull.Close() - } - - return luaRouter, luaRunner, cleanup -} - -// createTestRoutes creates test Lua scripts for benchmarking -func createTestRoutes(routesDir string) { - // Simple GET endpoint - getCode := []byte(`return "Hello, World!"`) - os.WriteFile(filepath.Join(routesDir, "GET.lua"), getCode, 0644) - - // POST endpoint with form handling - postCode := []byte(` - local data = ctx.form or {} - return "Received: " .. (data.message or "no message") - `) - os.WriteFile(filepath.Join(routesDir, "POST.lua"), postCode, 0644) - - // Computationally intensive endpoint - complexDir := filepath.Join(routesDir, "complex") - os.MkdirAll(complexDir, 0755) - complexCode := []byte(` - local result = {} - for i = 1, 1000 do - table.insert(result, i * i) - end - return "Calculated " .. #result .. " squared numbers" - `) - os.WriteFile(filepath.Join(complexDir, "GET.lua"), complexCode, 0644) - - // Create middleware for testing - middlewareCode := []byte(` - http.set_metadata("middleware_executed", true) - return nil - `) - os.WriteFile(filepath.Join(routesDir, "middleware.lua"), middlewareCode, 0644) - - // Nested middleware - nestedDir := filepath.Join(routesDir, "api") - os.MkdirAll(nestedDir, 0755) - nestedMiddlewareCode := []byte(` - http.set_metadata("api_middleware", true) - return nil - `) - os.WriteFile(filepath.Join(nestedDir, "middleware.lua"), nestedMiddlewareCode, 0644) - - // Nested endpoint - nestedEndpointCode := []byte(`return "API endpoint"`) - os.WriteFile(filepath.Join(nestedDir, "GET.lua"), nestedEndpointCode, 0644) -} - -// BenchmarkRouterLookup tests route lookup performance -func BenchmarkRouterLookup(b *testing.B) { - luaRouter, _, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/" - params := &routers.Params{} - - for b.Loop() { - _, _, _, _ = luaRouter.GetRouteInfo(method, path, params) - } -} - -// BenchmarkSimpleLuaExecution tests execution of a simple Lua script -func BenchmarkSimpleLuaExecution(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/" - params := &routers.Params{} - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - ctx := runner.NewContext() - defer ctx.Release() - - for b.Loop() { - _, _ = luaRunner.Run(bytecode, ctx, scriptPath) - } -} - -// BenchmarkComplexLuaExecution tests execution of a computation-heavy script -func BenchmarkComplexLuaExecution(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/complex" - params := &routers.Params{} - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - ctx := runner.NewContext() - defer ctx.Release() - - for b.Loop() { - _, _ = luaRunner.Run(bytecode, ctx, scriptPath) - } -} - -// BenchmarkGetEndpoint tests end-to-end processing for GET endpoint -func BenchmarkGetEndpoint(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/" - params := &routers.Params{} - - for b.Loop() { - // Route lookup - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - // Context setup - ctx := runner.NewContext() - - // Script execution - _, _ = luaRunner.Run(bytecode, ctx, scriptPath) - - // Cleanup - ctx.Release() - } -} - -// BenchmarkPostEndpoint tests end-to-end processing for POST endpoint -func BenchmarkPostEndpoint(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "POST" - path := "/" - params := &routers.Params{} - - for b.Loop() { - // Route lookup - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - // Context setup with form data - ctx := runner.NewContext() - ctx.Set("form", map[string]any{ - "message": "Hello from benchmark test", - }) - - // Script execution - _, _ = luaRunner.Run(bytecode, ctx, scriptPath) - - // Cleanup - ctx.Release() - } -} - -// BenchmarkConcurrentExecution tests parallel execution performance -func BenchmarkConcurrentExecution(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/" - params := &routers.Params{} - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - ctx := runner.NewContext() - _, _ = luaRunner.Run(bytecode, ctx, scriptPath) - ctx.Release() - } - }) -} - -// BenchmarkConcurrentComplexExecution tests parallel execution of intensive scripts -func BenchmarkConcurrentComplexExecution(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/complex" - params := &routers.Params{} - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - ctx := runner.NewContext() - _, _ = luaRunner.Run(bytecode, ctx, scriptPath) - ctx.Release() - } - }) -} - -// BenchmarkMiddlewareExecution tests middleware + handler execution -func BenchmarkMiddlewareExecution(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/api" - params := &routers.Params{} - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - for b.Loop() { - ctx := runner.NewContext() - - // Execute combined middleware + handler - response, _ := luaRunner.Run(bytecode, ctx, scriptPath) - if response != nil { - runner.ReleaseResponse(response) - } - - ctx.Release() - } -} - -// BenchmarkRouteCompilation tests the performance of route compilation -func BenchmarkRouteCompilation(b *testing.B) { - tempDir, err := os.MkdirTemp("", "moonshark-compile") - if err != nil { - b.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) - - routesDir := filepath.Join(tempDir, "routes") - os.MkdirAll(routesDir, 0755) - - for i := 0; i < b.N; i++ { - b.StopTimer() - os.RemoveAll(routesDir) - os.MkdirAll(routesDir, 0755) - createTestRoutes(routesDir) - b.StartTimer() - - // Creating router triggers compilation - _, _ = routers.NewLuaRouter(routesDir) - } -} - -// BenchmarkContextCreation measures the cost of creating execution contexts -func BenchmarkContextCreation(b *testing.B) { - for b.Loop() { - ctx := runner.NewContext() - ctx.Release() - } -} - -// BenchmarkContextWithData measures context creation with realistic data -func BenchmarkContextWithData(b *testing.B) { - for i := 0; i < b.N; i++ { - ctx := runner.NewContext() - ctx.Set("method", "POST") - ctx.Set("path", "/api/users") - ctx.Set("host", "example.com") - ctx.Set("params", map[string]any{"id": "123"}) - ctx.Set("form", map[string]any{ - "username": "testuser", - "email": "user@example.com", - "active": true, - }) - ctx.Release() - } -} - -// BenchmarkRunnerExecute tests the runner's Execute method with timeout -func BenchmarkRunnerExecute(b *testing.B) { - luaRouter, luaRunner, cleanup := setupTestEnv(b) - defer cleanup() - - method := "GET" - path := "/" - params := &routers.Params{} - bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params) - - for b.Loop() { - ctx := runner.NewContext() - _, _ = luaRunner.Execute(context.Background(), bytecode, ctx, scriptPath) - ctx.Release() - } -} diff --git a/utils/config/config.go b/utils/config/config.go index 8a2882d..c11afbf 100644 --- a/utils/config/config.go +++ b/utils/config/config.go @@ -14,9 +14,10 @@ import ( type Config struct { // Server settings Server struct { - Port int - Debug bool - HTTPLogging bool + Port int + Debug bool + HTTPLogging bool + StaticPrefix string } // Runner settings @@ -49,6 +50,7 @@ func New() *Config { config.Server.Port = 3117 config.Server.Debug = false config.Server.HTTPLogging = false + config.Server.StaticPrefix = "static/" // Runner defaults config.Runner.PoolSize = runtime.GOMAXPROCS(0) @@ -117,6 +119,9 @@ func applyConfig(config *Config, values map[string]any) { if v, ok := serverTable["http_logging"].(bool); ok { config.Server.HTTPLogging = v } + if v, ok := serverTable["static_prefix"].(string); ok { + config.Server.StaticPrefix = v + } } // Apply runner settings diff --git a/watchers/api.go b/watchers/api.go index 20bfb31..d745a7f 100644 --- a/watchers/api.go +++ b/watchers/api.go @@ -5,7 +5,7 @@ import ( "strings" "sync" - "Moonshark/routers" + "Moonshark/router" "Moonshark/runner" "Moonshark/utils/color" "Moonshark/utils/logger" @@ -34,7 +34,7 @@ func ShutdownWatcherManager() { } // WatchLuaRouter sets up a watcher for a LuaRouter's routes directory -func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) { +func WatchLuaRouter(router *router.LuaRouter, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) { manager := GetWatcherManager() config := DirectoryWatcherConfig{