// Command mwse is the MWSE engine: a WebSocket relay that virtualizes connected // peers so they can exchange data through rooms, pairings and data tunnels without // knowing one another's real identity. It is the Go rewrite of the original // Node.js engine; the on-the-wire SDK contract is unchanged. package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "git.saqut.com/saqut/mwse/internal/bridge" "git.saqut.com/saqut/mwse/internal/config" "git.saqut.com/saqut/mwse/internal/httpserver" "git.saqut.com/saqut/mwse/internal/services" "git.saqut.com/saqut/mwse/internal/ws" ) func main() { cfg := config.Load() hub := ws.NewHub() // Bridge (3rd-party server integration, #46) — optional; activated by env vars. // BRIDGE_APPROVE_URL: before each WebSocket upgrade, MWSE POSTs the candidate // client id + metadata there; a non-200 or {"approve":false} body rejects it. // BRIDGE_TRIGGER_URL: when a suit notification is replied to, MWSE POSTs the // reply there so the app is notified immediately instead of polling. var srvOpts httpserver.ServerOptions var svcOpts []services.Option if approveURL := os.Getenv("BRIDGE_APPROVE_URL"); approveURL != "" { srvOpts.Approver = bridge.NewHTTPApprover(approveURL, 0) log.Printf("bridge: connection approval delegated to %s", approveURL) } if triggerURL := os.Getenv("BRIDGE_TRIGGER_URL"); triggerURL != "" { svcOpts = append(svcOpts, services.WithNotifyTrigger(bridge.NewHTTPTrigger(triggerURL, 0))) log.Printf("bridge: suit-reply trigger wired to %s", triggerURL) } // The inbox is always created so bridge/send and POST /api/bridge/inbox are // available once either bridge env var is set, or if the inbox is otherwise // useful. When neither env var is set, the services option is simply not added // and the bridge/send handler is not registered. if srvOpts.Approver != nil || os.Getenv("BRIDGE_INBOX") != "" { inbox := bridge.NewInbox(0) svcOpts = append(svcOpts, services.WithBridgeInbox(inbox)) srvOpts.BridgeInbox = inbox log.Printf("bridge: inbox enabled (POST /api/bridge/inbox)") } reg := services.Register(hub, svcOpts...) // The notify and datastore stores hold entries with a TTL; their janitors // reclaim expired ones so memory cannot grow without bound. They are stopped // during shutdown so no goroutine is leaked. stopNotify := reg.Notify.StartJanitor(time.Minute) stopData := reg.Data.StartJanitor(time.Minute) defer stopNotify() defer stopData() srv := httpserver.New(hub, cfg, srvOpts) // Run the listener in the background so main can wait for a shutdown signal. serverErr := make(chan error, 1) go func() { log.Printf("MWSE engine listening on %s", cfg.Addr()) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { serverErr <- err } }() // Wait for SIGINT/SIGTERM or a fatal listener error. stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) select { case err := <-serverErr: log.Fatalf("MWSE engine failed: %v", err) case sig := <-stop: log.Printf("received %s, shutting down gracefully", sig) } // Graceful shutdown: stop accepting new HTTP work, then close every live // WebSocket connection so peers receive a clean close frame. ctx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("http shutdown error: %v", err) } hub.CloseAll() log.Printf("MWSE engine stopped") }