The provider registry¶
Providers make themselves reachable by name. A consumer names a platform in configuration, and the registry turns that string into a constructed provider without the consumer importing anything platform-specific.
func Register(name string, f Factory) error
func Unregister(name string)
func Lookup(name string) (Factory, bool)
func Registered() []string
func NewClient(_ context.Context, name string, cfg ClientConfig) (Client, error)
func New(ctx context.Context, name string, cfg Config) (*Provider, error) // deprecated
type Factory func(ClientConfig) (Client, error)
The registry is package-level state guarded by a sync.RWMutex. All six
functions are safe to call concurrently. There is exactly one registry per
process; there is no way to create a second, isolated one.
Register: claiming a name¶
| Condition | Result |
|---|---|
name is "" |
ErrInvalidName, nothing registered |
f is nil |
ErrNilFactory, nothing registered |
name already taken |
ErrAlreadyRegistered, the existing factory is kept |
| otherwise | nil, the factory is registered |
A duplicate is refused, not overwritten. Silently replacing would let a blank-imported provider displace another with no diagnostic, with initialisation order deciding the winner, so the failure would surface much later, as the wrong provider being used, a long way from its cause.
Registering returns an error rather than panicking, which leaves the decision
with the caller. A provider registering from init() has nothing sensible to do
with a failure and should panic at its own call site, where the panic names the
module at fault:
func init() {
if err := chatplatform.Register("discord", factory); err != nil {
panic("chat-platform-discord: " + err.Error())
}
}
Names are matched exactly. They are not normalised, lower-cased or trimmed, so
"Discord" and "discord" are two different providers.
Unregister: removing a name¶
Removes the factory registered under name. It has no return value and
removing a name that was never registered is not an error.
It exists for tests, which must be able to leave the registry as they found
it. Production code registers once from init() and never removes. Nothing
prevents you calling it at runtime, but a provider already constructed from a
factory keeps working; unregistering affects future lookups only.
Lookup: getting a factory by name¶
Returns the registered factory and true, or nil and false if the name is
unknown. It never returns a non-nil factory with false, so the two-value form
is safe to ignore only if you are certain of the name.
The commonest cause of false is a missing blank import. A provider registers
itself from init(), so a binary that never imports the provider module has
nothing registered under its name:
Registered: listing what is available¶
Returns every registered name, sorted, in a freshly allocated slice, so callers rendering them get a stable order and cannot mutate the registry by mutating the result.
Sorted rather than insertion-ordered because the natural uses are a help string and an error listing valid values, and both look broken if the order changes between runs:
if _, ok := chatplatform.Lookup(cfg.Platform); !ok {
return fmt.Errorf("unknown platform %q; available: %s",
cfg.Platform, strings.Join(chatplatform.Registered(), ", "))
}
The slice is empty, not nil, when nothing is registered.
NewClient: look up and build a transport¶
The one-call path for a consumer holding a platform name from configuration. It
looks the name up and calls the factory, which returns a Client: a transport
you then mint one Provider per space from, via Client.Provider.
It does not connect. Nothing on this path reaches the network, which is why
the context.Context is named _ in the implementation: lookup is an in-memory
map read, and a factory must not dial. Cancelling the context has no effect and
no deadline applies. It is part of the signature so a future registry lookup can
honour one without a breaking change.
The consequence worth acting on: ErrForbidden cannot come from here. A
platform cannot refuse a declared Need before anybody has asked it for one, so
a permission check belongs at Reader.Connect. What this returns is
ErrNotFound for an unknown name, or whatever the provider's own constructor
returns for a configuration it will not accept.
It returns ErrNotFound for an unknown provider name. That is the same
sentinel a provider returns for a message, thread or member that does not exist,
so errors.Is(err, chatplatform.ErrNotFound) on this result means "no such
platform" and nothing else. To tell an unknown name from a factory failure, use
Lookup and check the boolean:
f, ok := chatplatform.Lookup(name)
if !ok {
return fmt.Errorf("no provider registered as %q", name)
}
c, err := f(cfg) // any error here came from the provider
Not every transport is in the registry, and one is kept out on purpose. A
Factory takes a ClientConfig, and a ClientConfig carries a token. A
transport whose whole reason to exist is that the token never reaches it,
Discord's voice node, has a
configuration with no token field and is built by hand instead. The registry
is for a consumer holding a platform name; a consumer building a node knows
which one it is building.
New: the deprecated single-space path¶
Deprecated, and removed one minor release after the one that introduced
Client. Use NewClient and Client.Provider.
It still works, and returns a Provider that owns its transport: closing its
Reader closes the connection underneath, so it does not leak. That ownership is
also the problem. Calling it once per space dials a transport per space, and on a
platform carrying many spaces over one connection (Discord) that meets
connection rate limits in production rather than in a test with one space.
Unlike NewClient, this one does use its context: it forwards it to the
provider's Provider method, which may honour it.
Factory: what a provider supplies¶
A factory validates a ClientConfig and returns a Client: a transport, not a
scope. Spaces are named afterwards, through Client.Provider, which is what lets
several of them share one connection.
It must not open a connection, and must not require a working credential.
Connect is where the network happens. Both rules are load-bearing for testing:
the conformance harness constructs every provider with no token at all and never
dials, so a factory that authenticates eagerly cannot be conformance-tested at
all.
A factory returning both a nil client and a nil error is a contract breach;
the harness reports it, because a caller has nothing to check.