decoupling start and shutdown

- start is no longer a blocking call
- giving it back to users
- the Stop method now waits for the timeout before closing
This commit is contained in:
2026-06-25 16:02:47 -04:00
parent f5948eb36e
commit e6be288302
3 changed files with 51 additions and 41 deletions
+24 -33
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"log"
"os"
"os/signal"
"slices"
"strings"
"sync"
@@ -52,6 +51,8 @@ type Bolt struct {
maxRoutines int
//generic message handler func
msgHandlerf Payload
//command timeout when closing connection
grace time.Duration
}
// New creates a new bolt instance and applies any supplied options
@@ -73,6 +74,7 @@ func New(opts ...Option) (*Bolt, error) {
indicator: DEFAULT_INDICATOR,
wg: sync.WaitGroup{},
maxRoutines: DEFAULT_MAX_GOROUTINES,
grace: time.Second * 5,
}
b.Identify.Intents = DEFAULT_INTENTS
@@ -87,22 +89,33 @@ func New(opts ...Option) (*Bolt, error) {
}
// Start applies the message event handler function to the bot and opens the initial websocket connection
// with Discord. Start is a blocking call that also handles safe shutdown, on Interrupt, bolt will give
// command routines a window to finish before closing the connection.
// with Discord
func (b *Bolt) Start() error {
b.AddHandler(b.msgEventHandler)
err := b.Open()
if err != nil {
return fmt.Errorf("failed to open websocket connection with Discord: %e", err)
return b.Open()
}
// AddCommands registers command handlers. Any messages that begin with the command indicator will be forwarded
// to the handler
func (b *Bolt) AddCommands(cmd ...Command) {
for _, c := range cmd {
//we can use a normal map here since writing only takes place once
b.commands[c.Trigger] = c
}
}
sigChannel := make(chan os.Signal, 1)
signal.Notify(sigChannel, os.Interrupt)
<-sigChannel
// AddMessageHandler registers the generic message handler, any messages that are not commands will be forwarded
// to the handler
func (b *Bolt) AddMessageHandler(p Payload) {
b.msgHandlerf = p
}
//give handler routines a 5 second window to finish processes before closing connection
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
// Stop waits for the alloted grace period timeout before handling safe shutdown of the
// Discord connection
func (b *Bolt) Stop() error {
ctx, cancel := context.WithTimeout(context.Background(), b.grace)
defer cancel()
closeChan := make(chan struct{})
go func() {
b.wg.Wait()
@@ -115,25 +128,6 @@ func (b *Bolt) Start() error {
case <-closeChan:
}
return b.stop()
}
// AddCommands registers command handlers. Any messages that begin with the command indicator will be forwarded
// to the handler
func (b *Bolt) AddCommands(cmd ...Command) {
for _, c := range cmd {
b.commands[c.Trigger] = c
}
}
// AddMessageHandler registers the generic message handler, any messages that are not commands will be forwarded
// to the handler
func (b *Bolt) AddMessageHandler(p Payload) {
b.msgHandlerf = p
}
// stop closes the websocket connection to Discord
func (b *Bolt) stop() error {
return b.Close()
}
@@ -258,7 +252,6 @@ func (b *Bolt) handleMessage(event *Message) error {
// handleCommand maps the first word of the message to the command payload, if it exists. It then forwards the message
// data to the handler after checking the timeout and role restrictions. If restrictions have not been met a generic
// response is sent to the message
// TODO: accept a string for timeout/role rejection messages to allow customization
func (b *Bolt) handleCommand(msg *Message, lg int) error {
run, ok := b.commands[msg.Words[0][lg:]]
if !ok {
@@ -345,8 +338,6 @@ func (b *Bolt) remainingTimeout(timeout time.Time) string {
return fmt.Sprintf("%d%s", timeLeft, metric)
}
// checks if the author of msg has the correct role to run the requested command
// roleCheck loops through the provided user role ID's grabs the role "name" and ensures the role is present
// in the commands whitelist. If the role exists in the Command whitelist a true response is returned
func (b *Bolt) roleCheck(guild string, roles []string, s *dg.Session, run Command) (bool, error) {