Skip to content

Commit f2dced3

Browse files
committed
feat: update dependencies and improve dashboard extension
- Added indirect dependencies for `github.com/a-h/templ` and `nhooyr.io/websocket`. - Updated `github.com/xraph/forgeui` to v1.3.0 in multiple modules. - Enhanced the dashboard extension to prevent duplicate starts and improved routing logic. - Refactored streaming bridge registration to use resolver functions for manager and config. - Updated mock connection methods for consistency in tests. - Improved HTTP response wrapper to handle multiple header writes correctly.
1 parent 4e92fe3 commit f2dced3

18 files changed

Lines changed: 205 additions & 84 deletions

File tree

examples/webtransport/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313

1414
require (
1515
github.com/BurntSushi/toml v1.6.0 // indirect
16+
github.com/a-h/templ v0.3.977 // indirect
1617
github.com/armon/go-metrics v0.4.1 // indirect
1718
github.com/beorn7/perks v1.0.1 // indirect
1819
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -68,6 +69,7 @@ require (
6869
github.com/uptrace/bunrouter v1.0.23 // indirect
6970
github.com/x448/float16 v0.8.4 // indirect
7071
github.com/xraph/confy v0.1.0 // indirect
72+
github.com/xraph/forgeui v1.3.0 // indirect
7173
github.com/xraph/go-utils v1.0.0 // indirect
7274
github.com/xraph/vessel v1.0.0 // indirect
7375
go.uber.org/multierr v1.11.0 // indirect
@@ -92,6 +94,7 @@ require (
9294
k8s.io/klog/v2 v2.130.1 // indirect
9395
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
9496
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
97+
nhooyr.io/websocket v1.8.17 // indirect
9598
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
9699
sigs.k8s.io/randfill v1.0.0 // indirect
97100
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect

examples/webtransport/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2
33
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
44
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
55
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
6+
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
7+
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
68
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
79
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
810
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -274,6 +276,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
274276
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
275277
github.com/xraph/confy v0.1.0 h1:dAdI/ShnkU5PEXVsfR86swoWj6XjJ37BdPQpBIUmg9M=
276278
github.com/xraph/confy v0.1.0/go.mod h1:/uhVfKibPR+kn7MI9LkVVekk84NP0sxsKZ9sFQoQ5Kc=
279+
github.com/xraph/forgeui v1.3.0 h1:HPS6+7fndy0qMkD1tB1QNydTOAqQ7oXOTi30+TlXOok=
280+
github.com/xraph/forgeui v1.3.0/go.mod h1:2oXAltVMFHJJG0OmXNzcA/BAEE+8L36LGTlkkDgaPpA=
277281
github.com/xraph/go-utils v1.0.0 h1:P1jOvtDlC5xZyGtnIhypFfPUBgpfyrwESY4TK4P2I5g=
278282
github.com/xraph/go-utils v1.0.0/go.mod h1:yp+PD9dXSA7tA9Pxmuveg5E7Ht1iHIVov8yMvanMG7U=
279283
github.com/xraph/vessel v1.0.0 h1:n2q30d0OGPENpFfmOUgEuS99Y+X6b6WTfzdOHiE4Ds0=
@@ -386,6 +390,8 @@ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZ
386390
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
387391
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
388392
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
393+
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
394+
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
389395
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
390396
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
391397
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=

extensions/dashboard/extension.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ func (e *Extension) Register(app forge.App) error {
237237

238238
// Start starts the dashboard extension.
239239
func (e *Extension) Start(ctx context.Context) error {
240+
// Guard against duplicate Start() — the DI container may also call Start()
241+
// on type-registry services implementing di.Starter.
242+
if e.IsStarted() {
243+
return nil
244+
}
245+
240246
e.Logger().Info("starting dashboard extension")
241247

242248
// Auto-discover DashboardAware and BridgeAware extensions
@@ -879,8 +885,19 @@ func (e *Extension) registerRoutes() {
879885
// and {basePath}/bridge/stream/, and routed pages.
880886
// Note: No StripPrefix — forgeui's internal mux registers routes with the
881887
// basePath prefix, so full request paths must reach it unmodified.
888+
fuiHTTPHandler := e.fuiApp.Handler() // cache once — page routes register on the ForgeUI router, not the mux
882889
fuiHandler := func(ctx forge.Context) error {
883-
e.fuiApp.Handler().ServeHTTP(ctx.Response(), ctx.Request())
890+
r := ctx.Request()
891+
if r.URL.Path == base {
892+
// Base path exactly (e.g. "/dashboard") — route directly to the
893+
// ForgeUI router with path "/" to serve the root page. This bypasses
894+
// http.ServeMux which would otherwise issue a 301 trailing-slash redirect.
895+
r2 := r.Clone(r.Context())
896+
r2.URL.Path = "/"
897+
e.fuiApp.Router().ServeHTTP(ctx.Response(), r2)
898+
return nil
899+
}
900+
fuiHTTPHandler.ServeHTTP(ctx.Response(), r)
884901
return nil
885902
}
886903

extensions/gateway/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
1010
github.com/xraph/forge v1.2.0
1111
github.com/xraph/forge/extensions/discovery v0.9.7
12-
github.com/xraph/forgeui v1.2.0
12+
github.com/xraph/forgeui v1.3.0
1313
)
1414

1515
require (

extensions/gateway/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ github.com/xraph/farp v1.0.2 h1:xu9d5JB/4+F0s5TVe3MaXrQO98a/AEOgYN0p/8moZlU=
284284
github.com/xraph/farp v1.0.2/go.mod h1:Nlli8WUsxvQL5wXiJqcAn6OsUHBzKJxrl9JLJ9J6Wqo=
285285
github.com/xraph/forge v1.2.0 h1:+f5pAoWIa4vzFDD3j5b3qDk+X52/Hr1kyoV6CV8vx0g=
286286
github.com/xraph/forge v1.2.0/go.mod h1:n7mKRcLgNsGblsMDmpBrZNieMSe7dFmnGaA1O8sDZSw=
287-
github.com/xraph/forgeui v1.2.0 h1:3ti6abcQDDDdVM5Rgw4Bqgfw3Ot5zUDBjqgaucd2lgE=
288-
github.com/xraph/forgeui v1.2.0/go.mod h1:2oXAltVMFHJJG0OmXNzcA/BAEE+8L36LGTlkkDgaPpA=
287+
github.com/xraph/forgeui v1.3.0 h1:HPS6+7fndy0qMkD1tB1QNydTOAqQ7oXOTi30+TlXOok=
288+
github.com/xraph/forgeui v1.3.0/go.mod h1:2oXAltVMFHJJG0OmXNzcA/BAEE+8L36LGTlkkDgaPpA=
289289
github.com/xraph/go-utils v1.0.0 h1:P1jOvtDlC5xZyGtnIhypFfPUBgpfyrwESY4TK4P2I5g=
290290
github.com/xraph/go-utils v1.0.0/go.mod h1:yp+PD9dXSA7tA9Pxmuveg5E7Ht1iHIVov8yMvanMG7U=
291291
github.com/xraph/vessel v1.0.0 h1:n2q30d0OGPENpFfmOUgEuS99Y+X6b6WTfzdOHiE4Ds0=

extensions/streaming/dashboard/bridge.go

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import (
1313
)
1414

1515
// RegisterBridge registers all streaming bridge functions on the given bridge instance.
16-
func RegisterBridge(b *bridge.Bridge, manager internal.Manager, config internal.Config) error {
16+
// It accepts resolver functions so that values are resolved at request time (after all extensions are initialized).
17+
func RegisterBridge(b *bridge.Bridge, managerFn func() internal.Manager, configFn func() internal.Config) error {
1718
reg := &bridgeRegistry{
18-
manager: manager,
19-
config: config,
19+
managerFn: managerFn,
20+
configFn: configFn,
2021
}
2122

2223
// Stats
@@ -95,10 +96,11 @@ func RegisterBridge(b *bridge.Bridge, manager internal.Manager, config internal.
9596
return nil
9697
}
9798

98-
// bridgeRegistry holds references for bridge function handlers.
99+
// bridgeRegistry holds resolver functions for bridge function handlers.
100+
// Values are resolved at request time to avoid nil references during extension startup.
99101
type bridgeRegistry struct {
100-
manager internal.Manager
101-
config internal.Config
102+
managerFn func() internal.Manager
103+
configFn func() internal.Config
102104
}
103105

104106
// ---- Parameter types ----
@@ -193,8 +195,21 @@ type actionResult struct {
193195

194196
// ---- Handler implementations ----
195197

198+
// resolveManager returns the manager, or an error if it's not yet initialized.
199+
func (r *bridgeRegistry) resolveManager() (internal.Manager, error) {
200+
m := r.managerFn()
201+
if m == nil {
202+
return nil, errors.New("streaming manager not initialized")
203+
}
204+
return m, nil
205+
}
206+
196207
func (r *bridgeRegistry) getStats(ctx bridge.Context, params emptyParams) (*statsResponse, error) {
197-
stats, err := r.manager.GetStats(context.Background())
208+
manager, err := r.resolveManager()
209+
if err != nil {
210+
return nil, err
211+
}
212+
stats, err := manager.GetStats(context.Background())
198213
if err != nil {
199214
return nil, err
200215
}
@@ -211,7 +226,11 @@ func (r *bridgeRegistry) getStats(ctx bridge.Context, params emptyParams) (*stat
211226
}
212227

213228
func (r *bridgeRegistry) getRooms(ctx bridge.Context, params emptyParams) ([]roomResponse, error) {
214-
rooms, err := r.manager.ListRooms(context.Background())
229+
manager, err := r.resolveManager()
230+
if err != nil {
231+
return nil, err
232+
}
233+
rooms, err := manager.ListRooms(context.Background())
215234
if err != nil {
216235
return nil, err
217236
}
@@ -241,9 +260,14 @@ func (r *bridgeRegistry) getRoom(ctx bridge.Context, params roomIDParams) (*room
241260
return nil, errors.New("room_id is required")
242261
}
243262

263+
manager, err := r.resolveManager()
264+
if err != nil {
265+
return nil, err
266+
}
267+
244268
bgCtx := context.Background()
245269

246-
room, err := r.manager.GetRoom(bgCtx, params.RoomID)
270+
room, err := manager.GetRoom(bgCtx, params.RoomID)
247271
if err != nil {
248272
return nil, err
249273
}
@@ -263,7 +287,11 @@ func (r *bridgeRegistry) getRoom(ctx bridge.Context, params roomIDParams) (*room
263287
}
264288

265289
func (r *bridgeRegistry) getChannels(ctx bridge.Context, params emptyParams) ([]channelResponse, error) {
266-
channels, err := r.manager.ListChannels(context.Background())
290+
manager, err := r.resolveManager()
291+
if err != nil {
292+
return nil, err
293+
}
294+
channels, err := manager.ListChannels(context.Background())
267295
if err != nil {
268296
return nil, err
269297
}
@@ -286,7 +314,11 @@ func (r *bridgeRegistry) getChannels(ctx bridge.Context, params emptyParams) ([]
286314
}
287315

288316
func (r *bridgeRegistry) getConnections(ctx bridge.Context, params emptyParams) ([]connectionResponse, error) {
289-
conns := r.manager.GetAllConnections()
317+
manager, err := r.resolveManager()
318+
if err != nil {
319+
return nil, err
320+
}
321+
conns := manager.GetAllConnections()
290322
result := make([]connectionResponse, 0, len(conns))
291323

292324
for _, conn := range conns {
@@ -304,7 +336,11 @@ func (r *bridgeRegistry) getConnections(ctx bridge.Context, params emptyParams)
304336
}
305337

306338
func (r *bridgeRegistry) getPresence(ctx bridge.Context, params emptyParams) ([]presenceResponse, error) {
307-
conns := r.manager.GetAllConnections()
339+
manager, err := r.resolveManager()
340+
if err != nil {
341+
return nil, err
342+
}
343+
conns := manager.GetAllConnections()
308344
bgCtx := context.Background()
309345

310346
seen := make(map[string]bool)
@@ -318,7 +354,7 @@ func (r *bridgeRegistry) getPresence(ctx bridge.Context, params emptyParams) ([]
318354

319355
seen[uid] = true
320356

321-
presence, _ := r.manager.GetPresence(bgCtx, uid)
357+
presence, _ := manager.GetPresence(bgCtx, uid)
322358
resp := presenceResponse{
323359
UserID: uid,
324360
Status: "online",
@@ -346,6 +382,11 @@ func (r *bridgeRegistry) createRoom(ctx bridge.Context, params createRoomParams)
346382
return nil, errors.New("room owner is required")
347383
}
348384

385+
manager, err := r.resolveManager()
386+
if err != nil {
387+
return nil, err
388+
}
389+
349390
room := local.NewRoom(internal.RoomOptions{
350391
ID: uuid.New().String(),
351392
Name: params.Name,
@@ -354,7 +395,7 @@ func (r *bridgeRegistry) createRoom(ctx bridge.Context, params createRoomParams)
354395
Private: params.Private,
355396
})
356397

357-
if err := r.manager.CreateRoom(context.Background(), room); err != nil {
398+
if err := manager.CreateRoom(context.Background(), room); err != nil {
358399
return nil, err
359400
}
360401

@@ -370,7 +411,12 @@ func (r *bridgeRegistry) deleteRoom(ctx bridge.Context, params roomIDParams) (*a
370411
return nil, errors.New("room_id is required")
371412
}
372413

373-
if err := r.manager.DeleteRoom(context.Background(), params.RoomID); err != nil {
414+
manager, err := r.resolveManager()
415+
if err != nil {
416+
return nil, err
417+
}
418+
419+
if err := manager.DeleteRoom(context.Background(), params.RoomID); err != nil {
374420
return nil, err
375421
}
376422

@@ -389,6 +435,11 @@ func (r *bridgeRegistry) sendMessage(ctx bridge.Context, params sendMessageParam
389435
return nil, errors.New("message is required")
390436
}
391437

438+
manager, err := r.resolveManager()
439+
if err != nil {
440+
return nil, err
441+
}
442+
392443
msg := &internal.Message{
393444
ID: uuid.New().String(),
394445
Type: internal.MessageTypeMessage,
@@ -398,7 +449,7 @@ func (r *bridgeRegistry) sendMessage(ctx bridge.Context, params sendMessageParam
398449
Timestamp: time.Now(),
399450
}
400451

401-
if err := r.manager.BroadcastToRoom(context.Background(), params.RoomID, msg); err != nil {
452+
if err := manager.BroadcastToRoom(context.Background(), params.RoomID, msg); err != nil {
402453
return nil, err
403454
}
404455

@@ -410,18 +461,19 @@ func (r *bridgeRegistry) sendMessage(ctx bridge.Context, params sendMessageParam
410461
}
411462

412463
func (r *bridgeRegistry) getConfig(ctx bridge.Context, params emptyParams) (*configResponse, error) {
464+
config := r.configFn()
413465
return &configResponse{
414-
Backend: r.config.Backend,
415-
Distributed: r.config.EnableDistributed,
416-
Rooms: r.config.EnableRooms,
417-
Channels: r.config.EnableChannels,
418-
Presence: r.config.EnablePresence,
419-
TypingIndicators: r.config.EnableTypingIndicators,
420-
MessageHistory: r.config.EnableMessageHistory,
421-
SessionResumption: r.config.EnableSessionResumption,
422-
MaxConnsPerUser: r.config.MaxConnectionsPerUser,
423-
MaxRoomsPerUser: r.config.MaxRoomsPerUser,
424-
MaxChannelsPerUser: r.config.MaxChannelsPerUser,
425-
MaxMessageSize: r.config.MaxMessageSize,
466+
Backend: config.Backend,
467+
Distributed: config.EnableDistributed,
468+
Rooms: config.EnableRooms,
469+
Channels: config.EnableChannels,
470+
Presence: config.EnablePresence,
471+
TypingIndicators: config.EnableTypingIndicators,
472+
MessageHistory: config.EnableMessageHistory,
473+
SessionResumption: config.EnableSessionResumption,
474+
MaxConnsPerUser: config.MaxConnectionsPerUser,
475+
MaxRoomsPerUser: config.MaxRoomsPerUser,
476+
MaxChannelsPerUser: config.MaxChannelsPerUser,
477+
MaxMessageSize: config.MaxMessageSize,
426478
}, nil
427479
}

0 commit comments

Comments
 (0)