refactor(cef): inject live config payload builders
- commit
- d24a1f0c652592b5478a0e4326dd05793872b2c7
- author
- brice · · 2026-04-11 13:06 UTC
- parent
- feb43a6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +74 -29
M internal/infrastructure/cef/engine_init.go +9 -2
| @@ -71,7 +71,7 @@ func NewEngine( | |||
| 71 | 71 | } | |
| 72 | 72 | os.Args = savedArgs | |
| 73 | 73 | ||
| 74 | - | return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger) | |
| 74 | + | return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger, deps.CurrentConfigPayload, deps.DefaultConfigPayload) | |
| 75 | 75 | } | |
| 76 | 76 | ||
| 77 | 77 | func resolvedStateRoot(opts port.EngineOptions) string { | |
| @@ -153,6 +153,7 @@ func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logge | |||
| 153 | 153 | func wireEngine( | |
| 154 | 154 | ctx context.Context, eng *Engine, cfg RuntimeConfig, transcodingCfg TranscodingRuntimeConfig, | |
| 155 | 155 | windowlessFrameRate int32, audioFactory port.AudioOutputFactory, logger *zerolog.Logger, | |
| 156 | + | currentConfigPayload func() ([]byte, error), defaultConfigPayload func() ([]byte, error), | |
| 156 | 157 | ) (*Engine, error) { | |
| 157 | 158 | gl, err := newGLLoader() | |
| 158 | 159 | if err != nil { | |
| @@ -206,7 +207,13 @@ func wireEngine( | |||
| 206 | 207 | eng.transcoderState = transcoderState | |
| 207 | 208 | ||
| 208 | 209 | messageRouter := NewMessageRouter(ctx) | |
| 209 | - | schemeHandler := newDumbSchemeHandler(ctx, messageRouter, mediaTranscoder) | |
| 210 | + | schemeHandler := newDumbSchemeHandler( | |
| 211 | + | ctx, | |
| 212 | + | messageRouter, | |
| 213 | + | mediaTranscoder, | |
| 214 | + | currentConfigPayload, | |
| 215 | + | defaultConfigPayload, | |
| 216 | + | ) | |
| 210 | 217 | schemeHandler.setAssets(assets.WebUIAssets) | |
| 211 | 218 | ||
| 212 | 219 | // Bridge clipboard writes from CEF JS → GDK system clipboard. | |
| @@ -71,7 +71,7 @@ func NewEngine( | |||||
| 71 | } | 71 | } | ||
| 72 | os.Args = savedArgs | 72 | os.Args = savedArgs | ||
| 73 | 73 | ||||
| 74 | - | return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger) | 74 | + | return wireEngine(ctx, eng, cfg, transcodingCfg, windowlessFrameRate, audioFactory, logger, deps.CurrentConfigPayload, deps.DefaultConfigPayload) |
| 75 | } | 75 | } | ||
| 76 | 76 | ||||
| 77 | func resolvedStateRoot(opts port.EngineOptions) string { | 77 | func resolvedStateRoot(opts port.EngineOptions) string { | ||
| @@ -153,6 +153,7 @@ func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logge | |||||
| 153 | func wireEngine( | 153 | func wireEngine( | ||
| 154 | ctx context.Context, eng *Engine, cfg RuntimeConfig, transcodingCfg TranscodingRuntimeConfig, | 154 | ctx context.Context, eng *Engine, cfg RuntimeConfig, transcodingCfg TranscodingRuntimeConfig, | ||
| 155 | windowlessFrameRate int32, audioFactory port.AudioOutputFactory, logger *zerolog.Logger, | 155 | windowlessFrameRate int32, audioFactory port.AudioOutputFactory, logger *zerolog.Logger, | ||
| 156 | + | currentConfigPayload func() ([]byte, error), defaultConfigPayload func() ([]byte, error), | |||
| 156 | ) (*Engine, error) { | 157 | ) (*Engine, error) { | ||
| 157 | gl, err := newGLLoader() | 158 | gl, err := newGLLoader() | ||
| 158 | if err != nil { | 159 | if err != nil { | ||
| @@ -206,7 +207,13 @@ func wireEngine( | |||||
| 206 | eng.transcoderState = transcoderState | 207 | eng.transcoderState = transcoderState | ||
| 207 | 208 | ||||
| 208 | messageRouter := NewMessageRouter(ctx) | 209 | messageRouter := NewMessageRouter(ctx) | ||
| 209 | - | schemeHandler := newDumbSchemeHandler(ctx, messageRouter, mediaTranscoder) | 210 | + | schemeHandler := newDumbSchemeHandler( |
| 211 | + | ctx, | |||
| 212 | + | messageRouter, | |||
| 213 | + | mediaTranscoder, | |||
| 214 | + | currentConfigPayload, | |||
| 215 | + | defaultConfigPayload, | |||
| 216 | + | ) | |||
| 210 | schemeHandler.setAssets(assets.WebUIAssets) | 217 | schemeHandler.setAssets(assets.WebUIAssets) | ||
| 211 | 218 | ||||
| 212 | // Bridge clipboard writes from CEF JS → GDK system clipboard. | 219 | // Bridge clipboard writes from CEF JS → GDK system clipboard. | ||
M internal/infrastructure/cef/scheme_handler.go +35 -27
| @@ -19,8 +19,6 @@ import ( | |||
| 19 | 19 | purecef "github.com/bnema/purego-cef/cef" | |
| 20 | 20 | ||
| 21 | 21 | "github.com/bnema/dumber/internal/application/port" | |
| 22 | - | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 23 | - | "github.com/bnema/dumber/internal/infrastructure/env" | |
| 24 | 22 | "github.com/bnema/dumber/internal/infrastructure/webutil" | |
| 25 | 23 | "github.com/bnema/dumber/internal/logging" | |
| 26 | 24 | "github.com/rs/zerolog" | |
| @@ -47,15 +45,16 @@ var pageRootFiles = map[string]string{ | |||
| 47 | 45 | // dumbSchemeHandler serves both the conceptual dumb:// URLs and the actual | |
| 48 | 46 | // internal https://dumber.invalid origin used by CEF. | |
| 49 | 47 | type dumbSchemeHandler struct { | |
| 50 | - | ctx context.Context | |
| 51 | - | messageRouter *MessageRouter | |
| 52 | - | transcoder port.MediaTranscoder | |
| 53 | - | assets embed.FS | |
| 54 | - | assetsSet bool | |
| 55 | - | assetDir string | |
| 56 | - | logger zerolog.Logger | |
| 57 | - | hwSurveyor *env.HardwareSurveyor | |
| 58 | - | mu sync.RWMutex | |
| 48 | + | ctx context.Context | |
| 49 | + | messageRouter *MessageRouter | |
| 50 | + | transcoder port.MediaTranscoder | |
| 51 | + | assets embed.FS | |
| 52 | + | assetsSet bool | |
| 53 | + | assetDir string | |
| 54 | + | logger zerolog.Logger | |
| 55 | + | currentConfigPayload func() ([]byte, error) | |
| 56 | + | defaultConfigPayload func() ([]byte, error) | |
| 57 | + | mu sync.RWMutex | |
| 59 | 58 | ||
| 60 | 59 | // onClipboardSet is called when JS sends copied text via /api/clipboard-set. | |
| 61 | 60 | // Set by the engine to write to the GDK system clipboard. | |
| @@ -63,15 +62,22 @@ type dumbSchemeHandler struct { | |||
| 63 | 62 | } | |
| 64 | 63 | ||
| 65 | 64 | // newDumbSchemeHandler creates a handler for internal CEF pages. | |
| 66 | - | func newDumbSchemeHandler(ctx context.Context, router *MessageRouter, transcoder port.MediaTranscoder) *dumbSchemeHandler { | |
| 65 | + | func newDumbSchemeHandler( | |
| 66 | + | ctx context.Context, | |
| 67 | + | router *MessageRouter, | |
| 68 | + | transcoder port.MediaTranscoder, | |
| 69 | + | currentConfigPayload func() ([]byte, error), | |
| 70 | + | defaultConfigPayload func() ([]byte, error), | |
| 71 | + | ) *dumbSchemeHandler { | |
| 67 | 72 | log := logging.FromContext(ctx) | |
| 68 | 73 | return &dumbSchemeHandler{ | |
| 69 | - | ctx: ctx, | |
| 70 | - | messageRouter: router, | |
| 71 | - | transcoder: transcoder, | |
| 72 | - | assetDir: "webui", | |
| 73 | - | logger: log.With().Str("component", "scheme-handler").Logger(), | |
| 74 | - | hwSurveyor: env.NewHardwareSurveyor(), | |
| 74 | + | ctx: ctx, | |
| 75 | + | messageRouter: router, | |
| 76 | + | transcoder: transcoder, | |
| 77 | + | assetDir: "webui", | |
| 78 | + | logger: log.With().Str("component", "scheme-handler").Logger(), | |
| 79 | + | currentConfigPayload: currentConfigPayload, | |
| 80 | + | defaultConfigPayload: defaultConfigPayload, | |
| 75 | 81 | } | |
| 76 | 82 | } | |
| 77 | 83 | ||
| @@ -141,10 +147,10 @@ func (h *dumbSchemeHandler) handleAPI(method, path string, request purecef.Reque | |||
| 141 | 147 | return h.newRawResourceHandler(http.StatusOK, "application/json", resp) | |
| 142 | 148 | ||
| 143 | 149 | case path == "/api/config" && (method == "" || strings.EqualFold(method, "GET")): | |
| 144 | - | return h.handleConfigAPI(config.Get()) | |
| 150 | + | return h.handleConfigAPI(h.currentConfigPayload) | |
| 145 | 151 | ||
| 146 | 152 | case path == "/api/config/default" && (method == "" || strings.EqualFold(method, "GET")): | |
| 147 | - | return h.handleConfigAPI(config.DefaultConfig()) | |
| 153 | + | return h.handleConfigAPI(h.defaultConfigPayload) | |
| 148 | 154 | ||
| 149 | 155 | case path == "/api/transcode" && (method == "" || strings.EqualFold(method, "GET")): | |
| 150 | 156 | return h.handleTranscodeAPI(request) | |
| @@ -209,14 +215,16 @@ func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef. | |||
| 209 | 215 | }) | |
| 210 | 216 | } | |
| 211 | 217 | ||
| 212 | - | // handleConfigAPI returns a JSON response with the given config. | |
| 213 | - | func (h *dumbSchemeHandler) handleConfigAPI(cfg *config.Config) purecef.ResourceHandler { | |
| 214 | - | var hwInfo *port.HardwareInfo | |
| 215 | - | if h.hwSurveyor != nil { | |
| 216 | - | survey := h.hwSurveyor.Survey(context.Background()) | |
| 217 | - | hwInfo = &survey | |
| 218 | + | func resolveConfigPayload(build func() ([]byte, error)) ([]byte, error) { | |
| 219 | + | if build == nil { | |
| 220 | + | return nil, fmt.Errorf("config payload builder not configured") | |
| 218 | 221 | } | |
| 219 | - | data, err := json.Marshal(config.BuildWebUIConfigPayload(cfg, hwInfo)) | |
| 222 | + | return build() | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // handleConfigAPI returns a JSON response with the given config payload builder. | |
| 226 | + | func (h *dumbSchemeHandler) handleConfigAPI(build func() ([]byte, error)) purecef.ResourceHandler { | |
| 227 | + | data, err := resolveConfigPayload(build) | |
| 220 | 228 | if err != nil { | |
| 221 | 229 | return h.newJSONResourceHandler(http.StatusInternalServerError, map[string]string{"error": err.Error()}) | |
| 222 | 230 | } | |
| @@ -19,8 +19,6 @@ import ( | |||||
| 19 | purecef "github.com/bnema/purego-cef/cef" | 19 | purecef "github.com/bnema/purego-cef/cef" | ||
| 20 | 20 | ||||
| 21 | "github.com/bnema/dumber/internal/application/port" | 21 | "github.com/bnema/dumber/internal/application/port" | ||
| 22 | - | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 23 | - | "github.com/bnema/dumber/internal/infrastructure/env" | |||
| 24 | "github.com/bnema/dumber/internal/infrastructure/webutil" | 22 | "github.com/bnema/dumber/internal/infrastructure/webutil" | ||
| 25 | "github.com/bnema/dumber/internal/logging" | 23 | "github.com/bnema/dumber/internal/logging" | ||
| 26 | "github.com/rs/zerolog" | 24 | "github.com/rs/zerolog" | ||
| @@ -47,15 +45,16 @@ var pageRootFiles = map[string]string{ | |||||
| 47 | // dumbSchemeHandler serves both the conceptual dumb:// URLs and the actual | 45 | // dumbSchemeHandler serves both the conceptual dumb:// URLs and the actual | ||
| 48 | // internal https://dumber.invalid origin used by CEF. | 46 | // internal https://dumber.invalid origin used by CEF. | ||
| 49 | type dumbSchemeHandler struct { | 47 | type dumbSchemeHandler struct { | ||
| 50 | - | ctx context.Context | 48 | + | ctx context.Context |
| 51 | - | messageRouter *MessageRouter | 49 | + | messageRouter *MessageRouter |
| 52 | - | transcoder port.MediaTranscoder | 50 | + | transcoder port.MediaTranscoder |
| 53 | - | assets embed.FS | 51 | + | assets embed.FS |
| 54 | - | assetsSet bool | 52 | + | assetsSet bool |
| 55 | - | assetDir string | 53 | + | assetDir string |
| 56 | - | logger zerolog.Logger | 54 | + | logger zerolog.Logger |
| 57 | - | hwSurveyor *env.HardwareSurveyor | 55 | + | currentConfigPayload func() ([]byte, error) |
| 58 | - | mu sync.RWMutex | 56 | + | defaultConfigPayload func() ([]byte, error) |
| 57 | + | mu sync.RWMutex | |||
| 59 | 58 | ||||
| 60 | // onClipboardSet is called when JS sends copied text via /api/clipboard-set. | 59 | // onClipboardSet is called when JS sends copied text via /api/clipboard-set. | ||
| 61 | // Set by the engine to write to the GDK system clipboard. | 60 | // Set by the engine to write to the GDK system clipboard. | ||
| @@ -63,15 +62,22 @@ type dumbSchemeHandler struct { | |||||
| 63 | } | 62 | } | ||
| 64 | 63 | ||||
| 65 | // newDumbSchemeHandler creates a handler for internal CEF pages. | 64 | // newDumbSchemeHandler creates a handler for internal CEF pages. | ||
| 66 | - | func newDumbSchemeHandler(ctx context.Context, router *MessageRouter, transcoder port.MediaTranscoder) *dumbSchemeHandler { | 65 | + | func newDumbSchemeHandler( |
| 66 | + | ctx context.Context, | |||
| 67 | + | router *MessageRouter, | |||
| 68 | + | transcoder port.MediaTranscoder, | |||
| 69 | + | currentConfigPayload func() ([]byte, error), | |||
| 70 | + | defaultConfigPayload func() ([]byte, error), | |||
| 71 | + | ) *dumbSchemeHandler { | |||
| 67 | log := logging.FromContext(ctx) | 72 | log := logging.FromContext(ctx) | ||
| 68 | return &dumbSchemeHandler{ | 73 | return &dumbSchemeHandler{ | ||
| 69 | - | ctx: ctx, | 74 | + | ctx: ctx, |
| 70 | - | messageRouter: router, | 75 | + | messageRouter: router, |
| 71 | - | transcoder: transcoder, | 76 | + | transcoder: transcoder, |
| 72 | - | assetDir: "webui", | 77 | + | assetDir: "webui", |
| 73 | - | logger: log.With().Str("component", "scheme-handler").Logger(), | 78 | + | logger: log.With().Str("component", "scheme-handler").Logger(), |
| 74 | - | hwSurveyor: env.NewHardwareSurveyor(), | 79 | + | currentConfigPayload: currentConfigPayload, |
| 80 | + | defaultConfigPayload: defaultConfigPayload, | |||
| 75 | } | 81 | } | ||
| 76 | } | 82 | } | ||
| 77 | 83 | ||||
| @@ -141,10 +147,10 @@ func (h *dumbSchemeHandler) handleAPI(method, path string, request purecef.Reque | |||||
| 141 | return h.newRawResourceHandler(http.StatusOK, "application/json", resp) | 147 | return h.newRawResourceHandler(http.StatusOK, "application/json", resp) | ||
| 142 | 148 | ||||
| 143 | case path == "/api/config" && (method == "" || strings.EqualFold(method, "GET")): | 149 | case path == "/api/config" && (method == "" || strings.EqualFold(method, "GET")): | ||
| 144 | - | return h.handleConfigAPI(config.Get()) | 150 | + | return h.handleConfigAPI(h.currentConfigPayload) |
| 145 | 151 | ||||
| 146 | case path == "/api/config/default" && (method == "" || strings.EqualFold(method, "GET")): | 152 | case path == "/api/config/default" && (method == "" || strings.EqualFold(method, "GET")): | ||
| 147 | - | return h.handleConfigAPI(config.DefaultConfig()) | 153 | + | return h.handleConfigAPI(h.defaultConfigPayload) |
| 148 | 154 | ||||
| 149 | case path == "/api/transcode" && (method == "" || strings.EqualFold(method, "GET")): | 155 | case path == "/api/transcode" && (method == "" || strings.EqualFold(method, "GET")): | ||
| 150 | return h.handleTranscodeAPI(request) | 156 | return h.handleTranscodeAPI(request) | ||
| @@ -209,14 +215,16 @@ func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef. | |||||
| 209 | }) | 215 | }) | ||
| 210 | } | 216 | } | ||
| 211 | 217 | ||||
| 212 | - | // handleConfigAPI returns a JSON response with the given config. | 218 | + | func resolveConfigPayload(build func() ([]byte, error)) ([]byte, error) { |
| 213 | - | func (h *dumbSchemeHandler) handleConfigAPI(cfg *config.Config) purecef.ResourceHandler { | 219 | + | if build == nil { |
| 214 | - | var hwInfo *port.HardwareInfo | 220 | + | return nil, fmt.Errorf("config payload builder not configured") |
| 215 | - | if h.hwSurveyor != nil { | |||
| 216 | - | survey := h.hwSurveyor.Survey(context.Background()) | |||
| 217 | - | hwInfo = &survey | |||
| 218 | } | 221 | } | ||
| 219 | - | data, err := json.Marshal(config.BuildWebUIConfigPayload(cfg, hwInfo)) | 222 | + | return build() |
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // handleConfigAPI returns a JSON response with the given config payload builder. | |||
| 226 | + | func (h *dumbSchemeHandler) handleConfigAPI(build func() ([]byte, error)) purecef.ResourceHandler { | |||
| 227 | + | data, err := resolveConfigPayload(build) | |||
| 220 | if err != nil { | 228 | if err != nil { | ||
| 221 | return h.newJSONResourceHandler(http.StatusInternalServerError, map[string]string{"error": err.Error()}) | 229 | return h.newJSONResourceHandler(http.StatusInternalServerError, map[string]string{"error": err.Error()}) | ||
| 222 | } | 230 | } | ||
A internal/infrastructure/cef/scheme_handler_test.go +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestResolveConfigPayload_UsesInjectedBuilder(t *testing.T) { | |
| 11 | + | data, err := resolveConfigPayload(func() ([]byte, error) { | |
| 12 | + | return []byte(`{"engine_type":"cef"}`), nil | |
| 13 | + | }) | |
| 14 | + | require.NoError(t, err) | |
| 15 | + | require.JSONEq(t, `{"engine_type":"cef"}`, string(data)) | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func TestResolveConfigPayload_PropagatesBuilderError(t *testing.T) { | |
| 19 | + | want := errors.New("boom") | |
| 20 | + | _, err := resolveConfigPayload(func() ([]byte, error) { | |
| 21 | + | return nil, want | |
| 22 | + | }) | |
| 23 | + | require.ErrorIs(t, err, want) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func TestResolveConfigPayload_NilBuilderFails(t *testing.T) { | |
| 27 | + | _, err := resolveConfigPayload(nil) | |
| 28 | + | require.Error(t, err) | |
| 29 | + | require.Contains(t, err.Error(), "config payload builder") | |
| 30 | + | } | |
| @@ -0,0 +1,30 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestResolveConfigPayload_UsesInjectedBuilder(t *testing.T) { | |||
| 11 | + | data, err := resolveConfigPayload(func() ([]byte, error) { | |||
| 12 | + | return []byte(`{"engine_type":"cef"}`), nil | |||
| 13 | + | }) | |||
| 14 | + | require.NoError(t, err) | |||
| 15 | + | require.JSONEq(t, `{"engine_type":"cef"}`, string(data)) | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func TestResolveConfigPayload_PropagatesBuilderError(t *testing.T) { | |||
| 19 | + | want := errors.New("boom") | |||
| 20 | + | _, err := resolveConfigPayload(func() ([]byte, error) { | |||
| 21 | + | return nil, want | |||
| 22 | + | }) | |||
| 23 | + | require.ErrorIs(t, err, want) | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func TestResolveConfigPayload_NilBuilderFails(t *testing.T) { | |||
| 27 | + | _, err := resolveConfigPayload(nil) | |||
| 28 | + | require.Error(t, err) | |||
| 29 | + | require.Contains(t, err.Error(), "config payload builder") | |||
| 30 | + | } | |||