-
Notifications
You must be signed in to change notification settings - Fork 0
Rework project to use DI instead of Singleton approach #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4193ed4
feat: replace singleton appstate approach with manual dep injection
iwa e005fcc
feat: centralize into App struct
iwa 9982115
chore: reword some comments
iwa d1284b3
fix: send correct http code if method not allowed
iwa 6e08f05
feat: handle err when construction post request for ntfy
iwa 9fff371
chore: remove some useless comments
iwa d1a872d
feat: replace loops with modern maps.Copy
iwa 5517bf8
fix: usage of renamed SetBulkDomains function
iwa c784a99
refactor: create a Server struct for http server + handle possible er…
iwa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/http" | ||
| "time" | ||
|
|
||
| "github.com/iwa/Expira/internal/state" | ||
| ) | ||
|
|
||
| // Server wraps the HTTP server with graceful lifecycle management | ||
| type Server struct { | ||
| server *http.Server | ||
| errors chan error | ||
| } | ||
|
|
||
| func NewServer(addr string, store *state.DomainStore) *Server { | ||
| mux := http.NewServeMux() | ||
| mux.HandleFunc("/health", HealthHandler) | ||
| mux.HandleFunc("/status", StatusHandlerFactory(store)) | ||
|
|
||
| return &Server{ | ||
| server: &http.Server{ | ||
| Addr: addr, | ||
| Handler: mux, | ||
| }, | ||
| errors: make(chan error, 1), | ||
| } | ||
| } | ||
|
|
||
| // Start begins listening for HTTP requests in a goroutine | ||
| func (s *Server) Start() { | ||
| go func() { | ||
| fmt.Printf("[INFO] HTTP server starting on %s\n", s.server.Addr) | ||
| if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||
| s.errors <- fmt.Errorf("[ERROR] HTTP server error: %w", err) | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| // Errors returns a channel that receives server errors | ||
| func (s *Server) Errors() <-chan error { | ||
| return s.errors | ||
| } | ||
|
|
||
| // Shutdown gracefully stops the server with a timeout | ||
| func (s *Server) Shutdown(timeout time.Duration) error { | ||
| ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||
| defer cancel() | ||
| return s.server.Shutdown(ctx) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package app | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/iwa/Expira/internal/api" | ||
| "github.com/iwa/Expira/internal/cron" | ||
| "github.com/iwa/Expira/internal/state" | ||
| "github.com/iwa/Expira/internal/utils" | ||
| ) | ||
|
|
||
| // App holds the application's core dependencies | ||
| type App struct { | ||
| Config *state.Config | ||
| Store *state.DomainStore | ||
| } | ||
|
|
||
| // New creates and initializes a new App instance | ||
| func New() *App { | ||
| config, store := utils.LoadConfig() | ||
| return &App{ | ||
| Config: config, | ||
| Store: store, | ||
| } | ||
| } | ||
|
|
||
| // Start runs the application | ||
| func (app *App) Start() error { | ||
| // Initial domain update and notification | ||
| utils.UpdateDomains(app.Store) | ||
| utils.ReportStatusInConsole(app.Store) | ||
| utils.Notify(app.Store, app.Config) | ||
|
|
||
| // Start HTTP server | ||
| server := api.NewServer("0.0.0.0:8080", app.Store) | ||
| server.Start() | ||
|
|
||
| // Start cron job in goroutine | ||
| cronErrors := make(chan error, 1) | ||
| go func() { | ||
| cron.StartCronLoop(app.Store, app.Config) | ||
| cronErrors <- fmt.Errorf("cron loop unexpectedly stopped") | ||
| }() | ||
|
|
||
| // Block the main go routine with error handling | ||
| select { | ||
| case err := <-server.Errors(): | ||
| if shutdownErr := server.Shutdown(5 * time.Second); shutdownErr != nil { | ||
| return fmt.Errorf("server error: %w, shutdown error: %v", err, shutdownErr) | ||
| } | ||
| return err | ||
| case err := <-cronErrors: | ||
| if shutdownErr := server.Shutdown(5 * time.Second); shutdownErr != nil { | ||
| return fmt.Errorf("cron error: %w, shutdown error: %v", err, shutdownErr) | ||
| } | ||
| return err | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package state | ||
|
|
||
| // Config holds configuration loaded from environment variables at startup. | ||
| // This configuration should never change during the application lifecycle. | ||
| type Config struct { | ||
| NotificationDays []int | ||
|
|
||
| TelegramNotification bool | ||
| TelegramChatID string | ||
| TelegramToken string | ||
|
|
||
| DiscordNotification bool | ||
| DiscordWebhookURL string | ||
|
|
||
| NtfyNotification bool | ||
| NtfyURL string | ||
| } | ||
|
|
||
| // NewConfig creates a new Config instance with default values | ||
| func NewConfig() *Config { | ||
| return &Config{ | ||
| NotificationDays: []int{}, | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package state | ||
|
|
||
| import ( | ||
| "maps" | ||
| "sync" | ||
| ) | ||
|
|
||
| type DomainStore struct { | ||
| mu sync.RWMutex | ||
| domains map[string]Domain | ||
| } | ||
|
|
||
| func NewDomainStore() *DomainStore { | ||
| return &DomainStore{ | ||
| domains: make(map[string]Domain), | ||
| } | ||
| } | ||
|
|
||
| // Returns the domain and true if found, or an empty domain and false if not found. | ||
| func (ds *DomainStore) GetDomain(name string) (Domain, bool) { | ||
| ds.mu.RLock() | ||
| defer ds.mu.RUnlock() | ||
|
|
||
| domain, ok := ds.domains[name] | ||
| return domain, ok | ||
| } | ||
|
|
||
| // This method is thread-safe and can be called from multiple goroutines. | ||
| func (ds *DomainStore) SetDomain(name string, domain Domain) { | ||
| ds.mu.Lock() | ||
| defer ds.mu.Unlock() | ||
|
|
||
| ds.domains[name] = domain | ||
| } | ||
|
|
||
| // GetAllDomains returns a copy of all domains. | ||
| // This ensures the returned map cannot cause race conditions if modified by the caller. | ||
| func (ds *DomainStore) GetAllDomains() map[string]Domain { | ||
| ds.mu.RLock() | ||
| defer ds.mu.RUnlock() | ||
|
|
||
| // Create a copy to avoid exposing internal map | ||
| domainsCopy := make(map[string]Domain, len(ds.domains)) | ||
| maps.Copy(domainsCopy, ds.domains) | ||
|
|
||
| return domainsCopy | ||
| } | ||
|
|
||
| // SetBulkDomains replaces all domains with the provided map. | ||
| func (ds *DomainStore) SetBulkDomains(domains map[string]Domain) { | ||
| ds.mu.Lock() | ||
| defer ds.mu.Unlock() | ||
|
|
||
| domainsCopy := make(map[string]Domain, len(domains)) | ||
| maps.Copy(domainsCopy, domains) | ||
|
|
||
| ds.domains = domainsCopy | ||
| } | ||
|
|
||
| // Count returns the number of domains in the store | ||
| func (ds *DomainStore) Count() int { | ||
| ds.mu.RLock() | ||
| defer ds.mu.RUnlock() | ||
|
|
||
| return len(ds.domains) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.