05. ProviderRegistry: Roles and Internal Mechanisms
ProviderRegistry is the core component of the VC Knots plugin architecture. Rather than simply storing provider instances, it serves as the central engine responsible for merging providers during initialization, automatically resolving dependency injection (DI), and dynamically weaving Aspect-Oriented Programming (AOP)-style extensions into providers.
This chapter explains how ProviderRegistry internally processes and coordinates provider instances, including its design and implementation behavior.
1. Initialization and Merge Behavior
When a VC Knots context is initialized (initializeContext), the framework merges the built-in default provider instances with any custom provider instances supplied through options.providers, creating a single integrated ProviderRegistry.
The merge process consists of the following two stages.
1. Flattening and Single Provider Replacement
All default and user-defined provider instances are combined into a single list.
single: true(Single Provider): If anotherproviderwith the samekindalready exists, it is completely replaced by the newly registeredprovider.single: false(Multi Provider): Existingproviderinstances are preserved, allowing multiple implementations of the samekindto coexist.
2. Indexing and Multi Provider Priority
For Multi Providers, VC Knots determines the order in which providers will be evaluated.
- Newly merged providers (typically user-defined providers registered later) are inserted at the beginning of the resolution list.
- As a result, custom implementations are automatically evaluated before the default implementations.
2. ProviderRegistry Methods
ProviderRegistry exposes two primary methods to both application code and other provider instances.
get(kind)
Returns the registered provider for the specified kind.
- For Single Providers (
single: true), returns the single registered provider instance. - For Multi Providers (
single: false), returns an array of providers ordered by evaluation priority (newest first).
select(kind, value)
Selects the most appropriate provider from a Multi Provider collection according to a runtime condition.
Dynamic Resolution Flow
- Retrieve the list of Multi Providers for the specified
kind. - Iterate through the providers from highest to lowest priority.
- Call
canHandle(value)on each provider. - Return the first provider whose
canHandle(value)returnstrue. - If no provider matches, a resolution error is thrown.
This mechanism allows custom providers to take precedence while automatically falling back to the default provider when appropriate.
3. Dependency Injection and Lazy Resolution
When multiple provider instances collaborate, developers do not need to manually wire dependencies together. ProviderRegistry automatically injects itself into providers when necessary.
The withProviderRegistry Helper
To indicate that a custom provider supports automatic dependency injection, VC Knots provides the withProviderRegistry helper.
When implementing a custom provider, it is recommended to spread this helper into the returned object and combine it with the WithProviderRegistry type.
import { WithProviderRegistry, withProviderRegistry } from '@trustknots/vcknots'
export const customProvider = (): MyProvider & WithProviderRegistry => {
return {
...withProviderRegistry, // Expands the providers placeholder
kind: 'my-provider',
name: 'custom-provider',
single: true,
// ...
}
}
Automatic Injection Behavior
Whenever a provider is retrieved through ProviderRegistry (get() or select()), the registry performs the following steps.
- It checks whether the provider contains a
providerproperty supplied bywithProviderRegistry. - If present, it injects the current
ProviderRegistryinstance into that property.
This design provides several advantages.
-
Avoiding Circular Dependencies
Even if Provider A depends on Provider B and Provider B also depends on Provider A, both can be safely initialized without dependency ordering issues.
-
Lazy Resolution
Other providers are resolved only when they are actually needed. This reduces initialization overhead and always retrieves the latest active provider instance, even after providers have been replaced.
As a result, provider implementations can simply call this.providers.get('other-provider') or this.providers.select(...) to safely access any other registered provider.
4. Extension Weaving
VC Knots provides an Extension mechanism that injects cross-cutting concerns—such as logging, auditing, exception handling, or performance measurement—before or after provider method execution without modifying the provider implementation itself.
ProviderRegistry performs this weaving process immediately before returning a provider.
Example: How Weaving Works
The following example defines an extension that logs issuer metadata whenever it is retrieved.
import { CredentialIssuer, CredentialIssuerMetadata } from '../credential-issuer.types'
import { Extension } from './extension.types'
export const traceFetchedIssuerMetadata = (): Extension<
CredentialIssuer,
Promise<CredentialIssuerMetadata | null>
> => {
return {
// Target provider method
on: 'issuer-store-metadata-provider.fetch',
// Interceptor logic
async intercept(original, xs) {
const issuer = await original(xs)
if (issuer) {
console.log(JSON.stringify(issuer, null, '\t'))
}
return issuer
},
}
}
Once this extension is registered, ProviderRegistry automatically performs the following steps.
-
Monitor Provider Retrieval
When application code calls
context.providers.get('issuer-store-metadata-provider'), the registry checks all registered extensions. -
Generate a Wrapper (Weaving)
If an extension targets
issuer-store-metadata-provider.fetch, the registry creates a wrapper around the originalfetch()method instead of returning the original provider directly. -
Execute Transparently
When
fetch()is invoked, the extension'sintercept()method executes automatically, calls the original implementation, writes the JSON log, and returns the original result.
Benefits for Developers
Because weaving is handled transparently by ProviderRegistry, neither provider authors nor application developers need to write any special integration code.
- Provider developers can focus solely on implementing business logic such as DID resolution or metadata storage without embedding logging, monitoring, or auditing logic.
- Application developers simply use providers through the normal interfaces, while all registered extensions are automatically and transparently applied.