From 6966723259be5d1bf2d10d1578536feb7cafbeea Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 15:48:10 +0400 Subject: [PATCH 01/31] feat: Make CORS origins configurable via environment variables --- api/internal/interfaces/rest/routes.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index e9fa68f..d6b313a 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -1,6 +1,7 @@ package firestore_server import ( + cmn "Codex-Backend/api/internal/common" firestore_handlers "Codex-Backend/api/internal/interfaces/rest/handlers" firestore_middleware "Codex-Backend/api/internal/interfaces/rest/middleware" @@ -9,11 +10,20 @@ import ( ) func RegisteredRoutes(r *gin.Engine) { + debug_domain, err := cmn.GetEnvVariable("DEBUG_DOMAIN") + if err != nil { + debug_domain = "http://localhost:3000" + } + + release_domain, err := cmn.GetEnvVariable("RELEASE_DOMAIN") + if err != nil { + release_domain = "https://codex-reader.vercel.app" + } r.Use(cors.New(cors.Config{ AllowOrigins: []string{ - "http://localhost:3000", // Local - "https://codex-reader.vercel.app", // Remote TODO: change this to include url from env later. + debug_domain, // Local + release_domain, // Remote TODO: change this to include url from env later. }, AllowMethods: []string{ "GET", From ab3d4dd345c4290f7581422ecb5af011632e1b40 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 15:53:14 +0400 Subject: [PATCH 02/31] feat: Unify CORS origin configuration with single DOMAIN env var --- api/internal/interfaces/rest/routes.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index d6b313a..5c77143 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -10,20 +10,14 @@ import ( ) func RegisteredRoutes(r *gin.Engine) { - debug_domain, err := cmn.GetEnvVariable("DEBUG_DOMAIN") + domain, err := cmn.GetEnvVariable("DOMAIN") if err != nil { - debug_domain = "http://localhost:3000" - } - - release_domain, err := cmn.GetEnvVariable("RELEASE_DOMAIN") - if err != nil { - release_domain = "https://codex-reader.vercel.app" + panic(err) } r.Use(cors.New(cors.Config{ AllowOrigins: []string{ - debug_domain, // Local - release_domain, // Remote TODO: change this to include url from env later. + domain, }, AllowMethods: []string{ "GET", From 6843b5ffad5e39b92b2425dc8948e96c4c1526cf Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 15:53:14 +0400 Subject: [PATCH 03/31] feat: Expose Content-Type header in CORS configuration --- api/internal/interfaces/rest/routes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index 5c77143..631ad9b 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -38,6 +38,7 @@ func RegisteredRoutes(r *gin.Engine) { }, ExposeHeaders: []string{ "Content-Length", + "Content-Type", }, AllowCredentials: true, })) From 0161d757b8322271db9b6d09f0f7047d1be634c7 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 17:13:36 +0400 Subject: [PATCH 04/31] Adds Procfile for process management Defines commands for the web server and background worker. Enables deployment and process management on compatible platforms. --- Procfile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..502b31c --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: go run api/cmd/web/main.go +worker: go run api/cmd/worker/main.go From 4133c1a589ffb00864b23d361cca874e02af3074 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 17:14:07 +0400 Subject: [PATCH 05/31] Introduces dedicated worker process Renames the primary application entry point to `web/main.go` to clarify its role as the web server. Adds a new `worker/main.go` to house background and asynchronous tasks, enabling a more robust and scalable architecture. Updates environment variable loading in the web component to directly use `os.Getenv`, reducing coupling with the Gin framework's mode detection. --- api/cmd/codex/main.go | 23 ----------------------- api/cmd/codex/web/main.go | 19 +++++++++++++++++++ api/cmd/codex/worker/main.go | 11 +++++++++++ 3 files changed, 30 insertions(+), 23 deletions(-) delete mode 100644 api/cmd/codex/main.go create mode 100644 api/cmd/codex/web/main.go create mode 100644 api/cmd/codex/worker/main.go diff --git a/api/cmd/codex/main.go b/api/cmd/codex/main.go deleted file mode 100644 index f9fb079..0000000 --- a/api/cmd/codex/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "Codex-Backend/api/internal/common" - firestore_server "Codex-Backend/api/internal/interfaces/rest" - "log" - - "github.com/gin-gonic/gin" - _ "github.com/heroku/x/hmetrics/onload" -) - -func init() { - if gin.Mode() == gin.DebugMode { - err := common.LoadEnvVariables() - if err != nil { - log.Fatalf("Error loading env file: %s", err.Error()) - } - } -} - -func main() { - firestore_server.Server() -} diff --git a/api/cmd/codex/web/main.go b/api/cmd/codex/web/main.go new file mode 100644 index 0000000..f64186a --- /dev/null +++ b/api/cmd/codex/web/main.go @@ -0,0 +1,19 @@ +package main + +import ( + cmn "Codex-Backend/api/internal/common" + firestore_server "Codex-Backend/api/internal/interfaces/rest" + "os" + + _ "github.com/heroku/x/hmetrics/onload" +) + +func init() { + if mode := os.Getenv("GIN_MODE"); mode == "debug" { + cmn.LoadEnvVariables() + } +} + +func main() { + firestore_server.Server() +} diff --git a/api/cmd/codex/worker/main.go b/api/cmd/codex/worker/main.go new file mode 100644 index 0000000..fb755c7 --- /dev/null +++ b/api/cmd/codex/worker/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Println("Worker started") + os.Exit(0) +} From 0e0b2d6bef97c4088daa99ba6e84c4807105c300 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 17:15:03 +0400 Subject: [PATCH 06/31] Refactors environment variable handling Updates environment variable loading and retrieval functions to terminate the application with a fatal error if a variable is missing or loading fails. This change eliminates repetitive error handling in consuming code, ensuring critical configuration is present at application start-up. --- api/internal/common/env.go | 14 +++++++++----- api/internal/common/token.go | 5 +---- api/internal/infrastructure/client/client.go | 5 +---- api/internal/interfaces/rest/middleware/token.go | 6 ++---- api/internal/interfaces/rest/routes.go | 5 +---- api/internal/interfaces/rest/server.go | 5 +---- 6 files changed, 15 insertions(+), 25 deletions(-) diff --git a/api/internal/common/env.go b/api/internal/common/env.go index fa521a2..773e746 100644 --- a/api/internal/common/env.go +++ b/api/internal/common/env.go @@ -2,21 +2,25 @@ package common import ( "errors" + "log" "net/http" "os" "github.com/joho/godotenv" ) -func LoadEnvVariables() error { - return godotenv.Load(".env") +func LoadEnvVariables() { + err := godotenv.Load(".env") + if err != nil { + log.Fatal(&Error{Err: errors.New("Failed to load environment variables"), Status: http.StatusInternalServerError}) + } } -func GetEnvVariable(v string) (string, error) { +func GetEnvVariable(v string) string { env_variable := os.Getenv(v) if env_variable == "" { - return "", &Error{Err: errors.New("Environmental Variable " + v + " Not Found"), Status: http.StatusNotFound} + log.Fatal(&Error{Err: errors.New("Environmental Variable " + v + " Not Found"), Status: http.StatusNotFound}) } - return env_variable, nil + return env_variable } diff --git a/api/internal/common/token.go b/api/internal/common/token.go index 42ab15c..65675a3 100644 --- a/api/internal/common/token.go +++ b/api/internal/common/token.go @@ -10,10 +10,7 @@ import ( func GenerateToken(email string) (string, error) { - key, err := GetEnvVariable("JWT_SIGN_KEY") - if err != nil { - return "", err - } + key := GetEnvVariable("JWT_SIGN_KEY") signKey := []byte(key) expirationTime := time.Now().Add(time.Hour * 24) diff --git a/api/internal/infrastructure/client/client.go b/api/internal/infrastructure/client/client.go index de8f586..f15b10a 100644 --- a/api/internal/infrastructure/client/client.go +++ b/api/internal/infrastructure/client/client.go @@ -18,10 +18,7 @@ type Client struct { func FirestoreClient() (*firestore.Client, error) { ctx := context.Background() - credentials_json, err := cmn.GetEnvVariable("GOOGLE_CREDENTIALS") - if err != nil { - return nil, &cmn.Error{Err: errors.New("Firestore Client Error - GOOGLE_CREDENTIALS: " + err.Error()), Status: http.StatusInternalServerError} - } + credentials_json := cmn.GetEnvVariable("GOOGLE_CREDENTIALS") sa := option.WithCredentialsJSON([]byte(credentials_json)) diff --git a/api/internal/interfaces/rest/middleware/token.go b/api/internal/interfaces/rest/middleware/token.go index dd6241d..62a516f 100644 --- a/api/internal/interfaces/rest/middleware/token.go +++ b/api/internal/interfaces/rest/middleware/token.go @@ -2,6 +2,7 @@ package firestore_middleware import ( "Codex-Backend/api/internal/common" + cmn "Codex-Backend/api/internal/common" "Codex-Backend/api/internal/domain" firestore_client "Codex-Backend/api/internal/infrastructure/client" firestore_collections "Codex-Backend/api/internal/infrastructure/collections" @@ -29,10 +30,7 @@ func ValidateToken() gin.HandlerFunc { return nil, jwt.ErrSignatureInvalid } - key, err := common.GetEnvVariable("JWT_SIGN_KEY") - if err != nil { - return nil, err - } + key := cmn.GetEnvVariable("JWT_SIGN_KEY") return []byte(key), nil }) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index 631ad9b..3d7878c 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -10,10 +10,7 @@ import ( ) func RegisteredRoutes(r *gin.Engine) { - domain, err := cmn.GetEnvVariable("DOMAIN") - if err != nil { - panic(err) - } + domain := cmn.GetEnvVariable("DOMAIN") r.Use(cors.New(cors.Config{ AllowOrigins: []string{ diff --git a/api/internal/interfaces/rest/server.go b/api/internal/interfaces/rest/server.go index 391e9ec..5a738c5 100644 --- a/api/internal/interfaces/rest/server.go +++ b/api/internal/interfaces/rest/server.go @@ -7,10 +7,7 @@ import ( ) func Server() { - mode, err := cmn.GetEnvVariable("GIN_MODE") - if err != nil { - panic(err) - } + mode := cmn.GetEnvVariable("GIN_MODE") gin.SetMode(mode) From 408fdcd74c4994e6a9b79c168e778fe684ae76fa Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 18:45:55 +0400 Subject: [PATCH 07/31] Chore: Enhance CORS configuration for cookie handling --- api/internal/interfaces/rest/handlers/users.go | 2 +- api/internal/interfaces/rest/routes.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/internal/interfaces/rest/handlers/users.go b/api/internal/interfaces/rest/handlers/users.go index 8ccdce4..c036b76 100644 --- a/api/internal/interfaces/rest/handlers/users.go +++ b/api/internal/interfaces/rest/handlers/users.go @@ -80,7 +80,7 @@ func LoginUser(c *gin.Context) { return } - c.SetCookie("Authorization", token, 3600*24, "", "", true, true) + c.SetCookie("Authorization", token, 3600*24, "/", cmn.GetEnvVariable("DOMAIN"), true, true) c.JSON(http.StatusOK, gin.H{ "user": gin.H{ diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index 3d7878c..dfb8a62 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -31,11 +31,16 @@ func RegisteredRoutes(r *gin.Engine) { "X-Requested-With", "Authorization", "Accept", - "Acces-Control-Allow-Origin", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials", + "Set-Cookie", }, ExposeHeaders: []string{ "Content-Length", "Content-Type", + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials", + "Set-Cookie", }, AllowCredentials: true, })) From 579595c448efa48e042ff2a8aea5036724f3fba7 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 18:45:55 +0400 Subject: [PATCH 08/31] Refactor: Remove placeholder worker exit --- api/cmd/codex/worker/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/cmd/codex/worker/main.go b/api/cmd/codex/worker/main.go index fb755c7..13579c9 100644 --- a/api/cmd/codex/worker/main.go +++ b/api/cmd/codex/worker/main.go @@ -2,10 +2,8 @@ package main import ( "fmt" - "os" ) func main() { fmt.Println("Worker started") - os.Exit(0) } From 06bbf96db2d1129d8ad8dd752b0c6ba160d56c16 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Sun, 17 Aug 2025 18:45:55 +0400 Subject: [PATCH 09/31] Feature: Integrate Riverqueue for asynchronous task processing --- api/internal/common/queue.go | 34 ++++++++++++++++++++++++++++++++++ go.mod | 17 +++++++++++++++++ go.sum | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 api/internal/common/queue.go diff --git a/api/internal/common/queue.go b/api/internal/common/queue.go new file mode 100644 index 0000000..935e84f --- /dev/null +++ b/api/internal/common/queue.go @@ -0,0 +1,34 @@ +package common + +import ( + "context" + "log/slog" + "os" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/riverqueue/river" + "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivershared/util/slogutil" +) + +func initializeRiverClient(ctx context.Context) *river.Client[pgx.Tx] { + dbPool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) + if err != nil { + panic(err) + } + + workers := river.NewWorkers() + riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{ + Logger: slog.New(&slogutil.SlogMessageOnlyHandler{Level: slog.LevelWarn}), + Queues: map[string]river.QueueConfig{ + river.QueueDefault: {MaxWorkers: 100}, + }, + Workers: workers, + }) + if err != nil { + panic(err) + } + + return riverClient +} diff --git a/go.mod b/go.mod index 4166496..d3b5cfe 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,12 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/heroku/x v0.5.2 + github.com/jackc/pgx/v5 v5.7.5 github.com/joho/godotenv v1.5.1 github.com/oklog/ulid/v2 v2.1.1 + github.com/riverqueue/river v0.24.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0 + github.com/riverqueue/river/rivershared v0.24.0 github.com/timsims/pamphlet v0.1.6 golang.org/x/crypto v0.41.0 golang.org/x/time v0.10.0 @@ -35,6 +39,7 @@ require ( github.com/bytedance/sonic v1.12.9 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.0.0 // indirect @@ -49,6 +54,9 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -56,6 +64,14 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/riverqueue/river/riverdriver v0.24.0 // indirect + github.com/riverqueue/river/rivertype v0.24.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -64,6 +80,7 @@ require ( go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.uber.org/goleak v1.3.0 // indirect golang.org/x/arch v0.14.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect diff --git a/go.sum b/go.sum index 3002dfc..9397196 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,16 @@ github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPq github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/heroku/x v0.5.2 h1:B3g+m78yQk70Mhe1MsrHICgDJwJMZNVDkaQtLhsSA/U= github.com/heroku/x v0.5.2/go.mod h1:B025iaZU9I0gPJSsBYmipizkS5TKgX9NWEx8IQQYTuI= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -113,6 +123,18 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/riverqueue/river v0.24.0 h1:CesL6vymWgz0d+zNwtnSGRWaB+E8Dax+o9cxD7sUmKc= +github.com/riverqueue/river v0.24.0/go.mod h1:UZ3AxU5t6WtyqNssaea/AkRS8h/kJ+E9ImSB3xyb3ns= +github.com/riverqueue/river/riverdriver v0.24.0 h1:HqGgGkls11u+YKDA7cKOdYKlQwRNJyHuGa3UtOvpdT0= +github.com/riverqueue/river/riverdriver v0.24.0/go.mod h1:dEew9DDIKenNvzpm8Edw8+PkqP3c0zl1fKjiQTq2n/w= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0 h1:yV37OIbRrhRwIiGeRT7P4D3szhAemu87BgCf8gTCoU4= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0/go.mod h1:QfznySVKC4ljx53syd/bA/LRSsydAyuD3Q9/EbSniKA= +github.com/riverqueue/river/rivershared v0.24.0 h1:KysokksW75pug2a5RTOc6WESOupWmsylVc6VWvAx+4Y= +github.com/riverqueue/river/rivershared v0.24.0/go.mod h1:UIBfSdai0oWFlwFcoqG4DZX83iA/fLWTEBGrj7Oe1ho= +github.com/riverqueue/river/rivertype v0.24.0 h1:xrQZm/h6U8TBPyTsQPYD5leOapuoBAcdz30bdBwTqOg= +github.com/riverqueue/river/rivertype v0.24.0/go.mod h1:lmdl3vLNDfchDWbYdW2uAocIuwIN+ZaXqAukdSCFqWs= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= @@ -131,6 +153,16 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/timsims/pamphlet v0.1.6 h1:6EX2hJeU4GbAu3AclA1apxVVbPug6KuzXsIP1NqNVb8= github.com/timsims/pamphlet v0.1.6/go.mod h1:i3lq9uyxZeAn2HT1c2+5IJ50oc9tH1K31gHT3WDYi8M= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -158,6 +190,8 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 0abe34dde0a41cfb68b8efa0f611eef2da1c58d2 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Mon, 18 Aug 2025 22:39:34 +0400 Subject: [PATCH 10/31] Enhances HTML content cleaning Expands the list of HTML elements removed during content processing. This ensures a more focused and cleaner textual output by stripping out interactive, media, and code-related tags. --- api/internal/usecases/collections/novels.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/internal/usecases/collections/novels.go b/api/internal/usecases/collections/novels.go index bf08930..e23f0c5 100644 --- a/api/internal/usecases/collections/novels.go +++ b/api/internal/usecases/collections/novels.go @@ -25,7 +25,7 @@ func cleanHtml(input string) (string, error) { return "", err } - doc.Find("head, script, style, nav").Remove() + doc.Find("head, script, style, nav, a, img, code, pre").Remove() html, err := doc.Find("body").Html() if err != nil { From 0033862f6873c9eb2e480c9ceae19de9084cdeef Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Mon, 18 Aug 2025 22:54:53 +0400 Subject: [PATCH 11/31] feat: Define ProcessEPUBArgs for worker jobs --- api/internal/usecases/worker/args.go | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 api/internal/usecases/worker/args.go diff --git a/api/internal/usecases/worker/args.go b/api/internal/usecases/worker/args.go new file mode 100644 index 0000000..f410332 --- /dev/null +++ b/api/internal/usecases/worker/args.go @@ -0,0 +1,9 @@ +package worker + +import "mime/multipart" + +type ProcessEPUBArgs struct { + File *multipart.FileHeader `json:"file"` +} + +func (ProcessEPUBArgs) Kind() string { return "process_epub" } From 122b77fe378f0003d198fd572147e64f16db587d Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Mon, 18 Aug 2025 22:54:53 +0400 Subject: [PATCH 12/31] refactor: Export River client initialization function --- api/internal/common/queue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/internal/common/queue.go b/api/internal/common/queue.go index 935e84f..975ab12 100644 --- a/api/internal/common/queue.go +++ b/api/internal/common/queue.go @@ -12,7 +12,7 @@ import ( "github.com/riverqueue/river/rivershared/util/slogutil" ) -func initializeRiverClient(ctx context.Context) *river.Client[pgx.Tx] { +func InitializeRiverClient(ctx context.Context) *river.Client[pgx.Tx] { dbPool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { panic(err) From a4672ee736d42ff162aef4332e33d33e178dc69e Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Mon, 18 Aug 2025 22:54:54 +0400 Subject: [PATCH 13/31] feat: Enqueue EPUB processing as a background job --- .../interfaces/rest/handlers/novels.go | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/api/internal/interfaces/rest/handlers/novels.go b/api/internal/interfaces/rest/handlers/novels.go index 0983969..ff5b8f3 100644 --- a/api/internal/interfaces/rest/handlers/novels.go +++ b/api/internal/interfaces/rest/handlers/novels.go @@ -4,6 +4,7 @@ import ( cmn "Codex-Backend/api/internal/common" "Codex-Backend/api/internal/domain" firestore_services "Codex-Backend/api/internal/usecases/collections" + "Codex-Backend/api/internal/usecases/worker" "net/http" "strings" @@ -28,19 +29,26 @@ func EPUBNovel(c *gin.Context) { return } - err = firestore_services.CreateNovelFromEPUB(epubFile, ctx) - if e, ok := err.(*cmn.Error); ok { - c.AbortWithStatusJSON(e.StatusCode(), gin.H{ - "error": "Failed to upload EPUB file: " + e.Error(), - }) - return - } else if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to upload EPUB file: " + err.Error(), - }) + riverClient := cmn.InitializeRiverClient(ctx) + _, err = riverClient.Insert(ctx, worker.ProcessEPUBArgs{File: epubFile}, nil) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + // err = firestore_services.CreateNovelFromEPUB(epubFile, ctx) + // if e, ok := err.(*cmn.Error); ok { + // c.AbortWithStatusJSON(e.StatusCode(), gin.H{ + // "error": "Failed to upload EPUB file: " + e.Error(), + // }) + // return + // } else if err != nil { + // c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + // "error": "Failed to upload EPUB file: " + err.Error(), + // }) + // return + // } + c.JSON(http.StatusOK, gin.H{ "message": "EPUB file uploaded successfully", }) From de6397bddc26094a1be7e82784accf27cd81a1a9 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Mon, 18 Aug 2025 22:54:54 +0400 Subject: [PATCH 14/31] feat: Implement EPUB processing worker --- api/cmd/codex/worker/main.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/api/cmd/codex/worker/main.go b/api/cmd/codex/worker/main.go index 13579c9..c699069 100644 --- a/api/cmd/codex/worker/main.go +++ b/api/cmd/codex/worker/main.go @@ -1,9 +1,36 @@ package main import ( - "fmt" + cmn "Codex-Backend/api/internal/common" + firestore_services "Codex-Backend/api/internal/usecases/collections" + "Codex-Backend/api/internal/usecases/worker" + "context" + "log" + "os" + + "github.com/riverqueue/river" ) +func init() { + if mode := os.Getenv("GIN_MODE"); mode == "debug" { + cmn.LoadEnvVariables() + } +} + func main() { - fmt.Println("Worker started") + riverClient := cmn.InitializeRiverClient(context.Background()) + if err := riverClient.Start(context.Background()); err != nil { + log.Fatal(err) + } + + workers := river.NewWorkers() + river.AddWorker(workers, &EPUBWorker{}) +} + +type EPUBWorker struct { + river.WorkerDefaults[worker.ProcessEPUBArgs] +} + +func (w *EPUBWorker) Work(ctx context.Context, job *river.Job[worker.ProcessEPUBArgs]) error { + return firestore_services.CreateNovelFromEPUB(job.Args.File, ctx) } From 9fe0a521face867840748a1b7baa6661bdfffc62 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 00:54:44 +0400 Subject: [PATCH 15/31] Refines go.mod dependencies Updates module requirements by moving `rivershared` from a direct dependency to an indirect one. This reflects that the module is no longer directly imported but is still a transitive requirement. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d3b5cfe..fef7985 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/oklog/ulid/v2 v2.1.1 github.com/riverqueue/river v0.24.0 github.com/riverqueue/river/riverdriver/riverpgxv5 v0.24.0 - github.com/riverqueue/river/rivershared v0.24.0 github.com/timsims/pamphlet v0.1.6 golang.org/x/crypto v0.41.0 golang.org/x/time v0.10.0 @@ -66,6 +65,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/riverqueue/river/riverdriver v0.24.0 // indirect + github.com/riverqueue/river/rivershared v0.24.0 // indirect github.com/riverqueue/river/rivertype v0.24.0 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect From bae1c75eafe6f6ca6df79ff60be5fff5fe17b1a9 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 00:56:08 +0400 Subject: [PATCH 16/31] Refactors EPUB worker and queue management Centralizes the `EPUBWorker` implementation in a dedicated package for improved reusability. Updates the River client initialization to accept an external `Workers` object, providing more flexible worker management. Changes EPUB file processing to read the entire file into a byte slice before enqueueing, simplifying data handling for workers. Introduces a 32MB file size limit for uploads. Adjusts River queue configurations for improved resilience and resource management, including setting max attempts and worker limits. Renames command directories for cleaner project structure. --- api/cmd/codex/worker/main.go | 36 ------------------- api/cmd/{codex => }/web/main.go | 0 api/cmd/worker/main.go | 30 ++++++++++++++++ api/internal/common/queue.go | 15 ++++---- .../interfaces/rest/handlers/novels.go | 29 +++++++++++++-- api/internal/usecases/collections/novels.go | 15 +------- api/internal/usecases/worker/args.go | 17 +++++++-- 7 files changed, 82 insertions(+), 60 deletions(-) delete mode 100644 api/cmd/codex/worker/main.go rename api/cmd/{codex => }/web/main.go (100%) create mode 100644 api/cmd/worker/main.go diff --git a/api/cmd/codex/worker/main.go b/api/cmd/codex/worker/main.go deleted file mode 100644 index c699069..0000000 --- a/api/cmd/codex/worker/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - cmn "Codex-Backend/api/internal/common" - firestore_services "Codex-Backend/api/internal/usecases/collections" - "Codex-Backend/api/internal/usecases/worker" - "context" - "log" - "os" - - "github.com/riverqueue/river" -) - -func init() { - if mode := os.Getenv("GIN_MODE"); mode == "debug" { - cmn.LoadEnvVariables() - } -} - -func main() { - riverClient := cmn.InitializeRiverClient(context.Background()) - if err := riverClient.Start(context.Background()); err != nil { - log.Fatal(err) - } - - workers := river.NewWorkers() - river.AddWorker(workers, &EPUBWorker{}) -} - -type EPUBWorker struct { - river.WorkerDefaults[worker.ProcessEPUBArgs] -} - -func (w *EPUBWorker) Work(ctx context.Context, job *river.Job[worker.ProcessEPUBArgs]) error { - return firestore_services.CreateNovelFromEPUB(job.Args.File, ctx) -} diff --git a/api/cmd/codex/web/main.go b/api/cmd/web/main.go similarity index 100% rename from api/cmd/codex/web/main.go rename to api/cmd/web/main.go diff --git a/api/cmd/worker/main.go b/api/cmd/worker/main.go new file mode 100644 index 0000000..4b2c260 --- /dev/null +++ b/api/cmd/worker/main.go @@ -0,0 +1,30 @@ +package main + +import ( + cmn "Codex-Backend/api/internal/common" + "Codex-Backend/api/internal/usecases/worker" + "context" + "log" + "os" + + "github.com/riverqueue/river" +) + +func init() { + if mode := os.Getenv("GIN_MODE"); mode == "debug" { + cmn.LoadEnvVariables() + } +} + +func main() { + workers := river.NewWorkers() + river.AddWorker(workers, &worker.EPUBWorker{}) + riverClient := cmn.InitializeRiverClient(context.Background(), workers) + if err := riverClient.Start(context.Background()); err != nil { + log.Fatal(err) + } + + log.Println("River worker started successfully, waiting for jobs...") + + select {} +} diff --git a/api/internal/common/queue.go b/api/internal/common/queue.go index 975ab12..53ee1a8 100644 --- a/api/internal/common/queue.go +++ b/api/internal/common/queue.go @@ -9,22 +9,25 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/riverqueue/river" "github.com/riverqueue/river/riverdriver/riverpgxv5" - "github.com/riverqueue/river/rivershared/util/slogutil" ) -func InitializeRiverClient(ctx context.Context) *river.Client[pgx.Tx] { +func InitializeRiverClient(ctx context.Context, workers *river.Workers) *river.Client[pgx.Tx] { dbPool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { panic(err) } - workers := river.NewWorkers() + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{ - Logger: slog.New(&slogutil.SlogMessageOnlyHandler{Level: slog.LevelWarn}), + Logger: logger, Queues: map[string]river.QueueConfig{ - river.QueueDefault: {MaxWorkers: 100}, + river.QueueDefault: {MaxWorkers: 10}, }, - Workers: workers, + MaxAttempts: 3, + Workers: workers, }) if err != nil { panic(err) diff --git a/api/internal/interfaces/rest/handlers/novels.go b/api/internal/interfaces/rest/handlers/novels.go index ff5b8f3..24cd1c0 100644 --- a/api/internal/interfaces/rest/handlers/novels.go +++ b/api/internal/interfaces/rest/handlers/novels.go @@ -5,10 +5,12 @@ import ( "Codex-Backend/api/internal/domain" firestore_services "Codex-Backend/api/internal/usecases/collections" "Codex-Backend/api/internal/usecases/worker" + "io" "net/http" "strings" "github.com/gin-gonic/gin" + "github.com/riverqueue/river" ) func EPUBNovel(c *gin.Context) { @@ -29,8 +31,31 @@ func EPUBNovel(c *gin.Context) { return } - riverClient := cmn.InitializeRiverClient(ctx) - _, err = riverClient.Insert(ctx, worker.ProcessEPUBArgs{File: epubFile}, nil) + maxSize := int64(32 * 1024 * 1024) // 32MB limit + if epubFile.Size > maxSize { + c.JSON(http.StatusBadRequest, gin.H{"error": "File too large (max 32MB)"}) + return + } + + file, err := epubFile.Open() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "Failed to open EPUB file: " + err.Error(), + }) + return + } + defer file.Close() + + fileData, err := io.ReadAll(file) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file"}) + return + } + + workers := river.NewWorkers() + river.AddWorker(workers, &worker.EPUBWorker{}) + riverClient := cmn.InitializeRiverClient(ctx, workers) + _, err = riverClient.Insert(ctx, worker.ProcessEPUBArgs{File: fileData}, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/api/internal/usecases/collections/novels.go b/api/internal/usecases/collections/novels.go index e23f0c5..014e191 100644 --- a/api/internal/usecases/collections/novels.go +++ b/api/internal/usecases/collections/novels.go @@ -7,8 +7,6 @@ import ( firestore_collections "Codex-Backend/api/internal/infrastructure/collections" "context" "errors" - "io" - "mime/multipart" "net/http" "strings" "time" @@ -35,18 +33,7 @@ func cleanHtml(input string) (string, error) { return htmltomarkdown.ConvertString(html) } -func CreateNovelFromEPUB(epubFile *multipart.FileHeader, ctx context.Context) error { - multipartFIle, err := epubFile.Open() - if err != nil { - return err - } - defer multipartFIle.Close() - - data, err := io.ReadAll(multipartFIle) - if err != nil { - return err - } - +func CreateNovelFromEPUB(data []byte, ctx context.Context) error { parser, err := pamphlet.OpenBytes(data) if err != nil { return err diff --git a/api/internal/usecases/worker/args.go b/api/internal/usecases/worker/args.go index f410332..4b23265 100644 --- a/api/internal/usecases/worker/args.go +++ b/api/internal/usecases/worker/args.go @@ -1,9 +1,22 @@ package worker -import "mime/multipart" +import ( + firestore_services "Codex-Backend/api/internal/usecases/collections" + "context" + + "github.com/riverqueue/river" +) + +type EPUBWorker struct { + river.WorkerDefaults[ProcessEPUBArgs] +} + +func (w *EPUBWorker) Work(ctx context.Context, job *river.Job[ProcessEPUBArgs]) error { + return firestore_services.CreateNovelFromEPUB(job.Args.File, ctx) +} type ProcessEPUBArgs struct { - File *multipart.FileHeader `json:"file"` + File []byte `json:"file"` } func (ProcessEPUBArgs) Kind() string { return "process_epub" } From fdcdcba49d5221f281de16ee0f76e3a0028a5a2d Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 01:13:41 +0400 Subject: [PATCH 17/31] Improves chapter batch upload reliability Increases the timeout duration for individual batch operations to accommodate larger uploads. Ensures immediate context cancellation upon encountering errors during batch processing, improving resource cleanup. Adjusts inter-batch sleep duration and logic to reduce pressure on the Firestore API and optimize final batch completion. --- api/internal/infrastructure/collections/chapters.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/internal/infrastructure/collections/chapters.go b/api/internal/infrastructure/collections/chapters.go index 84af87f..78a1755 100644 --- a/api/internal/infrastructure/collections/chapters.go +++ b/api/internal/infrastructure/collections/chapters.go @@ -95,7 +95,7 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, for i := 0; i < len(chapters); i += chunkSize { subset := chapters[i:min(i+chunkSize, len(chapters))] - batchCtx, cancel := context.WithTimeout(ctx, 300*time.Second) + batchCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() bw := c.Client.BulkWriter(batchCtx) @@ -104,6 +104,7 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, for _, chap := range subset { job, err := bw.Set(coll.Doc(chap.ID), chap) if err != nil { + cancel() return &cmn.Error{ Err: fmt.Errorf("Firestore Client Error - Batch Upload Chapters - Enqueue failed for chapter %s: %w", chap.ID, err), Status: http.StatusInternalServerError, @@ -119,6 +120,7 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, for j, job := range jobs { if _, err := job.Results(); err != nil { chap := subset[j] + cancel() return &cmn.Error{ Err: fmt.Errorf("Firestore Client Error - Batch Upload Chapters - Write failed for chapter %s: %w", chap.ID, err), Status: http.StatusInternalServerError, @@ -126,7 +128,10 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, } } - time.Sleep(100 * time.Millisecond) + cancel() + if i+chunkSize < len(chapters) { + time.Sleep(200 * time.Millisecond) + } } return nil From 0afc08ff6f627e9debd331820f03beb12bcc0d7a Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 01:47:37 +0400 Subject: [PATCH 18/31] Updates README with detailed API documentation Provides comprehensive documentation for the backend API endpoints, categorized into Client, Manage, and User groups. Clarifies setup and run instructions, emphasizing the need to run both web and worker processes. Updates technology stack details, including database changes. --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1f5baf9..4d471a2 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,54 @@ # Codex Backend -Backend for Codex - novel reader app. +Backend for Codex - novel reading platform. -# Details +## Details -Codex-Backend is built in `GoLang`, using `Gin` for server and `AWS-dynamoDB` for database. +Codex-Backend is built in `GoLang`, using `Gin` for server and ~AWS-dynamoDB~ firestore (moving to Heroku Postgres) for database. It is deployed on `Heroku` (thats why the code is in api directory). -### Endpoints +air config is outdated and not recommended. use [Run](Run guide instead) -This will be updated later (the version before was already outdated) +## Run +run server: -### Run + ```bash + go run api/cmd/web/main.go + ``` -- run server: ```bash - go run api/cmd/codex/main.go + go run api/cmd/worker/main.go ``` + +Both are needed + +## Endpoints + +3 Groups of endpoints: Client, Manage and User. + +- Client is responsible for basic GET requests. +- Manage is responsible for Upload/Modification operations. +- User is responsible for user authentication, authorization and Registration (Delete is not yet implemented). + +### Client: base path followed by request path +- `/all` - Get all novels +- `/:novel` - Get a novel by id +- `/:novel/:chapter` - Get chapter from novel using both ids +- `/:novel/all` - Get all chapters from novel using id +- `/:novel/chapter` - Get cursor paginated chapters from novel using id + + Options: limit (max 100), cursor (chapter index (integer)) and sort ("asc" || "desc"). + + Defaults: limit=100, cursor=0, sort="desc" + +### Manage: `/manage` followed by request path +- `/upload` - Upload novel +- `/:novel` - Update novel +- `/:novel/:chapter` - Update chapter + +### User: `/user` followed by request path +- `/validate` - Validate user token +- `/login` - Login user +- `/register` - Register user +- `/logout` - Logout user From 61b2fcfbfb907daeda1ac6fd580ff9b3c10c38cc Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 02:15:52 +0400 Subject: [PATCH 19/31] Refines chapter cursor pagination logic Optimizes Firestore queries by consolidating the initial page load into a single request. Improves the accuracy of `nextCursor` determination and the chapter slicing process to ensure correct pagination. Streamlines error handling for better readability. --- .../infrastructure/collections/chapters.go | 53 +++++++------------ 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/api/internal/infrastructure/collections/chapters.go b/api/internal/infrastructure/collections/chapters.go index 78a1755..c9c2f30 100644 --- a/api/internal/infrastructure/collections/chapters.go +++ b/api/internal/infrastructure/collections/chapters.go @@ -21,30 +21,16 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont limit := min(max(options.Limit, 1), 100) snapshots := []*firestore.DocumentSnapshot{} + var err error if options.Cursor == 0 { - snaps, err := query.Limit(1).Documents(ctx).GetAll() - if err != nil { - return nil, err - } - - if len(snaps) == 0 { - return nil, &cmn.Error{ - Err: fmt.Errorf("Firestore Client Error - Get Paginated Chapters - No Chapters Found for Novel: %s", options.NovelID), - Status: http.StatusNotFound, - } - } - - snapshots, err = query.StartAt(snaps[0]).Limit(limit + 1).Documents(ctx).GetAll() - if err != nil { - return nil, err - } + snapshots, err = query.Limit(limit + 1).Documents(ctx).GetAll() } else { - var err error snapshots, err = query.StartAt(options.Cursor).Limit(limit + 1).Documents(ctx).GetAll() - if err != nil { - return nil, err - } + } + + if err != nil { + return nil, err } if len(snapshots) == 0 { @@ -54,22 +40,10 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont } } - nextCursor := 0 - if len(snapshots) > limit { - var chapter domain.Chapter - if err := snapshots[len(snapshots)-1].DataTo(&chapter); err != nil { - return nil, err - } - nextCursor = chapter.Index - } - - snapLen := len(snapshots) - 1 - if snapLen <= 0 { - snapLen++ - } + actualLimit := min(len(snapshots), limit) + chapters := make([]domain.FrontendChapter, 0, actualLimit) - chapters := []domain.FrontendChapter{} - for _, snapshot := range snapshots[:snapLen] { + for _, snapshot := range snapshots[:actualLimit] { var chapter domain.Chapter if err := snapshot.DataTo(&chapter); err != nil { return nil, err @@ -82,6 +56,15 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont }) } + nextCursor := 0 + if len(snapshots) > limit { + var lastChapter domain.Chapter + if err := snapshots[limit].DataTo(&lastChapter); err != nil { + return nil, err + } + nextCursor = lastChapter.Index + } + return &domain.CursorResponse{ Chapters: chapters, NextCursor: nextCursor, From 7f66c4e510775ce57ee59271d0dc4fac9505faaf Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 17:05:26 +0400 Subject: [PATCH 20/31] Implements worker graceful shutdown Ensures the River worker can shut down cleanly upon receiving termination signals. Listens for SIGINT and SIGTERM to initiate a controlled shutdown of the River client, allowing in-flight jobs to complete within a timeout period and preventing abrupt process termination. --- api/cmd/worker/main.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/api/cmd/worker/main.go b/api/cmd/worker/main.go index 4b2c260..d961dfa 100644 --- a/api/cmd/worker/main.go +++ b/api/cmd/worker/main.go @@ -6,6 +6,9 @@ import ( "context" "log" "os" + "os/signal" + "syscall" + "time" "github.com/riverqueue/river" ) @@ -17,14 +20,35 @@ func init() { } func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + workers := river.NewWorkers() river.AddWorker(workers, &worker.EPUBWorker{}) - riverClient := cmn.InitializeRiverClient(context.Background(), workers) - if err := riverClient.Start(context.Background()); err != nil { - log.Fatal(err) + + riverClient := cmn.InitializeRiverClient(ctx, workers) + if err := riverClient.Start(ctx); err != nil { + log.Fatal("Failed to start River client:", err) } log.Println("River worker started successfully, waiting for jobs...") + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + <-sigChan + log.Println("Shutdown signal received, starting graceful shutdown...") + + cancel() + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer shutdownCancel() + + if err := riverClient.Stop(shutdownCtx); err != nil { + log.Printf("Error during River client shutdown: %v", err) + } else { + log.Println("River worker stopped gracefully") + } + select {} } From c4e2192e9872389fcc81e1af9c6b5ff8b327b4cf Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 17:13:49 +0400 Subject: [PATCH 21/31] Allows wildcard CORS for development Simplifies local development and testing by preventing CORS issues. Sets `AllowOrigins` to `*` when in debug mode or if the `DOMAIN` environment variable is not configured, allowing requests from any origin. --- api/internal/interfaces/rest/routes.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index dfb8a62..892e9bc 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -11,6 +11,9 @@ import ( func RegisteredRoutes(r *gin.Engine) { domain := cmn.GetEnvVariable("DOMAIN") + if gin.Mode() == gin.DebugMode || domain == "" { + domain = "*" + } r.Use(cors.New(cors.Config{ AllowOrigins: []string{ From d7f0bfe8802845eb3a66b4048615f11710adc71b Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 17:15:23 +0400 Subject: [PATCH 22/31] Refines wildcard domain assignment logic Ensures the wildcard domain (`*`) is only applied when the application is in debug mode and no specific domain is configured. This prevents unintended broad domain matching in production or non-debug environments. --- api/internal/interfaces/rest/routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index 892e9bc..973da74 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -11,7 +11,7 @@ import ( func RegisteredRoutes(r *gin.Engine) { domain := cmn.GetEnvVariable("DOMAIN") - if gin.Mode() == gin.DebugMode || domain == "" { + if gin.Mode() == gin.DebugMode && domain == "" { domain = "*" } From b745e1f1eeb2c9884503a3e5b37ecc6484abd409 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 20:21:11 +0400 Subject: [PATCH 23/31] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4d471a2..d312c94 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ air config is outdated and not recommended. use [Run](Run guide instead) ## Run run server: - ```bash - go run api/cmd/web/main.go - ``` +```bash +go run api/cmd/web/main.go +``` - ```bash - go run api/cmd/worker/main.go - ``` +```bash +go run api/cmd/worker/main.go +``` Both are needed From a0326edf0516b212b26011839fe93e2fecc6f331 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Tue, 19 Aug 2025 20:22:10 +0400 Subject: [PATCH 24/31] Adds graceful API server shutdown Configures the Gin-based HTTP server with proper read, write, and idle timeouts. Enables the server to handle OS signals (SIGINT, SIGTERM) to initiate a controlled shutdown. Ensures ongoing requests can complete within a timeout before the server fully exits, improving reliability during restarts or deployments. Removes a redundant `select {}` from the worker's main function. --- api/cmd/worker/main.go | 2 -- api/internal/interfaces/rest/server.go | 47 ++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/api/cmd/worker/main.go b/api/cmd/worker/main.go index d961dfa..c88464e 100644 --- a/api/cmd/worker/main.go +++ b/api/cmd/worker/main.go @@ -49,6 +49,4 @@ func main() { } else { log.Println("River worker stopped gracefully") } - - select {} } diff --git a/api/internal/interfaces/rest/server.go b/api/internal/interfaces/rest/server.go index 5a738c5..1740f26 100644 --- a/api/internal/interfaces/rest/server.go +++ b/api/internal/interfaces/rest/server.go @@ -2,18 +2,59 @@ package firestore_server import ( cmn "Codex-Backend/api/internal/common" + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" "github.com/gin-gonic/gin" ) func Server() { mode := cmn.GetEnvVariable("GIN_MODE") - gin.SetMode(mode) r := gin.Default() - RegisteredRoutes(r) - r.Run() + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + srv := &http.Server{ + Addr: ":" + port, + Handler: r, + + ReadTimeout: 15 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + go func() { + log.Printf("Starting server on port %s", port) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start server: %v", err) + } + }() + + log.Println("Server started successfully") + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + <-sigChan + log.Println("Shutdown signal received, starting graceful shutdown...") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Printf("Server forced to shutdown: %v", err) + } else { + log.Println("Server stopped gracefully") + } + } From a1760b28e7ada4c39810a555c9daab716feb2105 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Wed, 20 Aug 2025 21:15:37 +0400 Subject: [PATCH 25/31] Uses compiled binaries for processes Updates Procfile commands to execute pre-built binaries instead of directly running Go source files. Improves startup performance and aligns with production deployment practices. --- Procfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Procfile b/Procfile index 502b31c..cbaefa8 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ -web: go run api/cmd/web/main.go -worker: go run api/cmd/worker/main.go +web: ./bin/web +worker: ./bin/worker From e9a5c581fa63995b9ee12a6aa68d9d72c2a8d713 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Wed, 20 Aug 2025 21:30:54 +0400 Subject: [PATCH 26/31] refactor: Delegate chapter ID/timestamp generation in batch upload --- api/internal/usecases/collections/chapters.go | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/api/internal/usecases/collections/chapters.go b/api/internal/usecases/collections/chapters.go index 1eb7f27..57bf043 100644 --- a/api/internal/usecases/collections/chapters.go +++ b/api/internal/usecases/collections/chapters.go @@ -41,30 +41,14 @@ func BatchUploadChapters(novelId string, chapters []domain.Chapter, ctx context. c := firestore_collections.Client{Client: client} - final := []domain.Chapter{} - - for _, chapter := range chapters { - id, err := cmn.GenerateID("chapter") - if err != nil { - return err - } - - chapter.ID = id - chapter.CreatedAt = time.Now().Format("2006-01-02 15:04:05") - chapter.UpdatedAt = time.Now().Format("2006-01-02 15:04:05") - chapter.Deleted = false - - final = append(final, chapter) - } - - if len(final) == 0 { + if len(chapters) == 0 { return &cmn.Error{ Err: errors.New("Nothing to upload"), Status: http.StatusInternalServerError, } } - err = c.BatchUploadChapters(novelId, final, ctx) + err = c.BatchUploadChapters(novelId, chapters, ctx) if err != nil { return err } From 5475abe75f3c77bd9a7c0cf691c5d28f053d24bc Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Wed, 20 Aug 2025 21:30:54 +0400 Subject: [PATCH 27/31] feat: Remove batch chapter upload endpoint --- .../interfaces/rest/handlers/chapters.go | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/api/internal/interfaces/rest/handlers/chapters.go b/api/internal/interfaces/rest/handlers/chapters.go index 23b7902..70f7021 100644 --- a/api/internal/interfaces/rest/handlers/chapters.go +++ b/api/internal/interfaces/rest/handlers/chapters.go @@ -137,52 +137,6 @@ func FindAllChapters(c *gin.Context) { }) } -func BatchUploadChapters(c *gin.Context) { - ctx := c.Request.Context() - defer ctx.Done() - - novelId := c.Param("novel") - - if novelId == "" { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ - "error": "Novel ID not found", - }) - return - } - - var chapters []domain.Chapter - if err := c.ShouldBindJSON(&chapters); err != nil { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ - "error": "Failed to get chapters data: " + err.Error(), - }) - return - } - - if len(chapters) == 0 { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ - "error": "Nothing to upload", - }) - return - } - - err := firestore_services.BatchUploadChapters(novelId, chapters, ctx) - if e, ok := err.(*cmn.Error); ok { - c.AbortWithStatusJSON(e.StatusCode(), gin.H{ - "error": "Failed to upload chapters: " + e.Error(), - }) - return - } else if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to upload chapters: " + err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Chapters uploaded successfully", - }) -} - func CreateChapter(c *gin.Context) { ctx := c.Request.Context() defer ctx.Done() From 80c6f1f6d1f0aff228b6957240a2248db3c54006 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Wed, 20 Aug 2025 21:30:54 +0400 Subject: [PATCH 28/31] refactor: Categorize manage routes with comments --- api/internal/interfaces/rest/routes.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/internal/interfaces/rest/routes.go b/api/internal/interfaces/rest/routes.go index 973da74..782d6c9 100644 --- a/api/internal/interfaces/rest/routes.go +++ b/api/internal/interfaces/rest/routes.go @@ -62,11 +62,16 @@ func RegisteredRoutes(r *gin.Engine) { manage := r.Group("/manage") { + // Create manage.POST("/novel", firestore_middleware.ValidateToken(), firestore_handlers.CreateNovel) manage.POST("/:novel/chapter", firestore_middleware.ValidateToken(), firestore_handlers.CreateChapter) manage.POST("/epub", firestore_middleware.ValidateToken(), firestore_handlers.EPUBNovel) + + // Update manage.PUT("/:novel", firestore_middleware.ValidateToken(), firestore_handlers.UpdateNovel) manage.PUT("/:novel/:chapter", firestore_middleware.ValidateToken(), firestore_handlers.UpdateChapter) + + // Delete manage.DELETE("/:novel", firestore_middleware.ValidateToken(), firestore_handlers.DeleteNovel) manage.DELETE("/:novel/:chapter", firestore_middleware.ValidateToken(), firestore_handlers.DeleteChapter) } From 3bc63b4ae661b061734838a741056a2c1baa1697 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Wed, 20 Aug 2025 21:44:13 +0400 Subject: [PATCH 29/31] Removed commented code --- api/internal/interfaces/rest/handlers/novels.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/api/internal/interfaces/rest/handlers/novels.go b/api/internal/interfaces/rest/handlers/novels.go index 24cd1c0..ce38675 100644 --- a/api/internal/interfaces/rest/handlers/novels.go +++ b/api/internal/interfaces/rest/handlers/novels.go @@ -61,19 +61,6 @@ func EPUBNovel(c *gin.Context) { return } - // err = firestore_services.CreateNovelFromEPUB(epubFile, ctx) - // if e, ok := err.(*cmn.Error); ok { - // c.AbortWithStatusJSON(e.StatusCode(), gin.H{ - // "error": "Failed to upload EPUB file: " + e.Error(), - // }) - // return - // } else if err != nil { - // c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ - // "error": "Failed to upload EPUB file: " + err.Error(), - // }) - // return - // } - c.JSON(http.StatusOK, gin.H{ "message": "EPUB file uploaded successfully", }) From 189ddbaf37543cf62321414ef6c0d4de22b64d14 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Wed, 20 Aug 2025 22:04:22 +0400 Subject: [PATCH 30/31] Implements singleton River client initialization Ensures the River client is initialized only once using a `sync.Once` pattern. Registers essential workers, such as the EPUB processing worker, during the initial setup. Provides a centralized `GetRiverClient` function for controlled access to the client instance. Restructures the file to `river/client.go` for improved organization. --- .../common/{queue.go => river/client.go} | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) rename api/internal/common/{queue.go => river/client.go} (60%) diff --git a/api/internal/common/queue.go b/api/internal/common/river/client.go similarity index 60% rename from api/internal/common/queue.go rename to api/internal/common/river/client.go index 53ee1a8..58f2b42 100644 --- a/api/internal/common/queue.go +++ b/api/internal/common/river/client.go @@ -1,9 +1,12 @@ -package common +package queue import ( + "Codex-Backend/api/internal/usecases/worker" "context" + "log" "log/slog" "os" + "sync" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" @@ -11,6 +14,11 @@ import ( "github.com/riverqueue/river/riverdriver/riverpgxv5" ) +var ( + riverClient *river.Client[pgx.Tx] + riverOnce sync.Once +) + func InitializeRiverClient(ctx context.Context, workers *river.Workers) *river.Client[pgx.Tx] { dbPool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) if err != nil { @@ -35,3 +43,18 @@ func InitializeRiverClient(ctx context.Context, workers *river.Workers) *river.C return riverClient } + +func GetRiverClient(ctx context.Context) *river.Client[pgx.Tx] { + riverOnce.Do(func() { + log.Println("Initializing River client...") + + workers := river.NewWorkers() + river.AddWorker(workers, &worker.EPUBWorker{}) + + riverClient = InitializeRiverClient(ctx, workers) + + log.Println("River client initialized successfully") + }) + + return riverClient +} From c53ee27ea0736712dd73adb09acb3be794b6e106 Mon Sep 17 00:00:00 2001 From: Aleksandre Date: Wed, 20 Aug 2025 22:05:28 +0400 Subject: [PATCH 31/31] Unifies River queue client setup Centralizes River queue client and worker initialization into a dedicated package. Eliminates redundant setup logic across various application components, improving resource utilization and simplifying queue interactions. --- api/cmd/worker/main.go | 9 ++------- api/internal/interfaces/rest/handlers/novels.go | 6 ++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/api/cmd/worker/main.go b/api/cmd/worker/main.go index c88464e..47cdf16 100644 --- a/api/cmd/worker/main.go +++ b/api/cmd/worker/main.go @@ -2,15 +2,13 @@ package main import ( cmn "Codex-Backend/api/internal/common" - "Codex-Backend/api/internal/usecases/worker" + queue "Codex-Backend/api/internal/common/river" "context" "log" "os" "os/signal" "syscall" "time" - - "github.com/riverqueue/river" ) func init() { @@ -23,10 +21,7 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - workers := river.NewWorkers() - river.AddWorker(workers, &worker.EPUBWorker{}) - - riverClient := cmn.InitializeRiverClient(ctx, workers) + riverClient := queue.GetRiverClient(ctx) if err := riverClient.Start(ctx); err != nil { log.Fatal("Failed to start River client:", err) } diff --git a/api/internal/interfaces/rest/handlers/novels.go b/api/internal/interfaces/rest/handlers/novels.go index ce38675..48bc890 100644 --- a/api/internal/interfaces/rest/handlers/novels.go +++ b/api/internal/interfaces/rest/handlers/novels.go @@ -2,6 +2,7 @@ package firestore_handlers import ( cmn "Codex-Backend/api/internal/common" + queue "Codex-Backend/api/internal/common/river" "Codex-Backend/api/internal/domain" firestore_services "Codex-Backend/api/internal/usecases/collections" "Codex-Backend/api/internal/usecases/worker" @@ -10,7 +11,6 @@ import ( "strings" "github.com/gin-gonic/gin" - "github.com/riverqueue/river" ) func EPUBNovel(c *gin.Context) { @@ -52,9 +52,7 @@ func EPUBNovel(c *gin.Context) { return } - workers := river.NewWorkers() - river.AddWorker(workers, &worker.EPUBWorker{}) - riverClient := cmn.InitializeRiverClient(ctx, workers) + riverClient := queue.GetRiverClient(ctx) _, err = riverClient.Insert(ctx, worker.ProcessEPUBArgs{File: fileData}, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})