Repository: libp2p/go-libp2p
Pull request #2956 (2024-09-10 02:39:28)

feat: Add WithFxOption
While doing some refactoring of the p2p-forge client, I found that I wanted a way to get access to parts of the host earlier than `.New()` returning. I also wanted an option I could provide the constructor that would give me the host (or peer.ID, or something else) without having to have the user explicitly pass it to me.

In other words I think it's nicer to do this:
```go
// someServiceThatDependsOnHostAndConfiguresHost
service := NewServiceThatDependsOnHostAndConfiguresHost()
h, err := New(service.Libp2pOptions())
```

Rather than
```go
// someServiceThatDependsOnHostAndConfiguresHost
service := NewServiceThatDependsOnHostAndConfiguresHost()
h, err := New(service.Libp2pOptions())
service.ProvideHost(h)
```

And this change would enable the former (along with many other things as well).

The biggest downside here is that fx is really powerful and this could cause some confusing errors to users. 

Another concern is that it ties us to fx as long as we support this option. But I'm not worried about this for two reasons:
1. Fx is pretty nice, and as long as it doesn't break, I don't see a reason to move away from it.
2. I've marked it as experimental so we can feel less bad about break this.

Wondertan: Another option would be to provide libp2p options to users who rely on FX as well, but instead of libp2p controlling the `fx.New` and the user supplying FX options to libp2p, the libp2p provides itself as an option that users can import and inject into their `fx.New` and dep graph. 
MarcoPolo: > Another option would be to provide libp2p options to users who rely on FX as well, but instead of libp2p controlling the `fx.New` and the user supplying FX options to libp2p, the libp2p provides itself as an option that users can import and inject into their `fx.New` and dep graph.

Yes. I'm working on this as well. It will appear as effectively a new constructor. This option allows hooking into the existing constructor. Long term, providing FX options is a lot more modular and where I want to end up.

Would Celestia be interested in trying that API? Useful for me to gauge interest here :)


Patch:
diff --git a/config/config.go b/config/config.go
index 07bee93c..fb5a2ab1 100644
--- a/config/config.go
+++ b/config/config.go
@@ -36,6 +36,7 @@ import (
 	circuitv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"
 	relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
 	"github.com/libp2p/go-libp2p/p2p/protocol/holepunch"
+	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
 	"github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
 	libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
 	"github.com/prometheus/client_golang/prometheus"
@@ -142,6 +143,8 @@ type Config struct {
 	CustomUDPBlackHoleSuccessCounter  bool
 	IPv6BlackHoleSuccessCounter       *swarm.BlackHoleSuccessCounter
 	CustomIPv6BlackHoleSuccessCounter bool
+
+	UserFxOptions []fx.Option
 }
 
 func (cfg *Config) makeSwarm(eventBus event.Bus, enableMetrics bool) (*swarm.Swarm, error) {
@@ -482,6 +485,9 @@ func (cfg *Config) NewNode() (host.Host, error) {
 			return sw, nil
 		}),
 		fx.Provide(cfg.newBasicHost),
+		fx.Provide(func(bh *bhost.BasicHost) identify.IDService {
+			return bh.IDService()
+		}),
 		fx.Provide(func(bh *bhost.BasicHost) host.Host {
 			return bh
 		}),
@@ -536,6 +542,8 @@ func (cfg *Config) NewNode() (host.Host, error) {
 		fxopts = append(fxopts, fx.Invoke(func(bho *routed.RoutedHost) { rh = bho }))
 	}
 
+	fxopts = append(fxopts, cfg.UserFxOptions...)
+
 	app := fx.New(fxopts...)
 	if err := app.Start(context.Background()); err != nil {
 		return nil, err
diff --git a/options.go b/options.go
index 8e720137..4fbf8eb2 100644
--- a/options.go
+++ b/options.go
@@ -634,3 +634,12 @@ func IPv6BlackHoleSuccessCounter(f *swarm.BlackHoleSuccessCounter) Option {
 		return nil
 	}
 }
+
+// WithFxOption adds a user provided fx.Option to the libp2p constructor.
+// Experimental: This option is subject to change or removal.
+func WithFxOption(opts ...fx.Option) Option {
+	return func(cfg *Config) error {
+		cfg.UserFxOptions = append(cfg.UserFxOptions, opts...)
+		return nil
+	}
+}


Test patch:
diff --git a/fx_options_test.go b/fx_options_test.go
new file mode 100644
index 00000000..48ac79b5
--- /dev/null
+++ b/fx_options_test.go
@@ -0,0 +1,60 @@
+package libp2p
+
+import (
+	"testing"
+
+	"github.com/libp2p/go-libp2p/core/event"
+	"github.com/libp2p/go-libp2p/core/host"
+	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/fx"
+)
+
+func TestGetPeerID(t *testing.T) {
+	var id peer.ID
+	host, err := New(
+		WithFxOption(fx.Populate(&id)),
+	)
+	require.NoError(t, err)
+	defer host.Close()
+
+	require.Equal(t, host.ID(), id)
+
+}
+
+func TestGetEventBus(t *testing.T) {
+	var eb event.Bus
+	host, err := New(
+		NoTransports,
+		WithFxOption(fx.Populate(&eb)),
+	)
+	require.NoError(t, err)
+	defer host.Close()
+
+	require.NotNil(t, eb)
+}
+
+func TestGetHost(t *testing.T) {
+	var h host.Host
+	host, err := New(
+		NoTransports,
+		WithFxOption(fx.Populate(&h)),
+	)
+	require.NoError(t, err)
+	defer host.Close()
+
+	require.NotNil(t, h)
+}
+
+func TestGetIDService(t *testing.T) {
+	var id identify.IDService
+	host, err := New(
+		NoTransports,
+		WithFxOption(fx.Populate(&id)),
+	)
+	require.NoError(t, err)
+	defer host.Close()
+
+	require.NotNil(t, id)
+}