-
Notifications
You must be signed in to change notification settings - Fork 204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: initial plugin api #907
feat: initial plugin api #907
Conversation
Codecov Report
@@ Coverage Diff @@
## main #907 +/- ##
==========================================
+ Coverage 87.65% 87.78% +0.12%
==========================================
Files 470 477 +7
Lines 11172 11303 +131
Branches 1868 1870 +2
==========================================
+ Hits 9793 9922 +129
- Misses 1313 1315 +2
Partials 66 66
Continue to review full report at Codecov.
|
03196e9
to
bb664e2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome PR! Great setup for the modularisation :) Could of questions, but nothing blocking.
bb664e2
to
67ba97a
Compare
@blu3beri updated PR based on your feedback. Thanks for the suggestions, I like the appraoch! :) (also updated the PR body examples) |
0f56e8c
to
263f790
Compare
As a first comment (full review coming up): I remember we discussed this in a WG a few months back. To me, the usage of the terms ModulesThe term PluginsFurthermore I'd like to make a naming distinction between internal and external code. I haven't reviewed this in depth yet, but it looks like you're using the term Suggestion
|
Another one: Maybe this will become clear if I run through the entire code base, but just as a quick question... Say I have the following fictional module/plugin (I'll stick to the naming suggested in my previous comment for clarity):
In this scenario I might like to expose My reasoning for this is two-fold:
As a reference here is how DeepKit does this. They explicitly 'export' files that need to be exposed to other modules. Keen to hear your thoughts! |
Thanks for the insights @karimStekelenburg! I think you're right in addressing the difference between modules and plugins. Renaming them to Api sounds good to me, but I would like to postpone that for now. This PR intentionally doesn't introduce any breaking changes (yet). However we can already take it into account for the names of the decorators. What I don't share your thoughts with is to not use internal plugins. I think if we're making an API to dynamically register resources we should try to leverage it, instead of creating a second way to do this internally. I kinda like this approach from ACA-Py also, where internal modules are also plugins, it's just part of the So I think we need a way to register a plugin ( // just an example, syntax t.b.d.
class Module {
api = [MyApi],
services = [MyService, MyRepository]
} Would you think that works better? Re the export feature of deepkit, although nice that would be quite complex to implement at the moment. If I understand the docs correctly they have separate injection containers per module, and that's how they can make sure you can't access the classes from another module. I don't think that will be easy to do with TSyringe. I do see your point here though. Especially if we are going to extract modules, we should see changes to services as breaking changes currently as those are often used by other modules. I think we should open a separate issue and address this as part of modularization instead of multi-tenancy |
22ab6d6
to
b038a6d
Compare
@blu3beri @karimStekelenburg I've updated my initial comment |
Signed-off-by: Timo Glastra <[email protected]>
b038a6d
to
45557db
Compare
I really like this approach, especially after the very latest changes you introduced, which make it simpler an cleaner from a framework user perspective. Although I'm still not sure if it's better to use the term 'plug-in' or 'module'. I have a question, mainly because I'm not familiar with Tsyringe magic, about the access of services and repositories: once the module/plug-in is registered, would it be possible to do |
We're having constant discussion about this 😄, if you have opinions please share! @karimStekelenburg is of the opinion that we should only use the term plugin for external code/modules and shouldn't have internal plug-ins. I don't share that opinion, but I'm fine with both. I think we should rename the current module classes to API soon as I do agree that a module is more than it's public api.
That would be possible, yes. Currently we don't have any scoping of services and how you can access them (something we should look at). The registerContextScoped vs registerSingleton means just when a new instance will be returned. singletons will be shared between base agent and all tenants. context scoped will be created per tenant/base agent. #881 makes all services stateless, meaning we can use them as true singletons (by passing around the agent context) |
Nice work @TimoGlastra, I really like the I think if we do the |
@TimoGlastra & @genaris, my opinion on using the term I'm quite happy with the current name changes and don't really mind if we call external 'add-ons' |
Ok in that case I propose we merge this PR and mark the feature as EXPERIMENTAL, that means we can make breaking changes to it without bumping a new minor release. If we decide on what is better (plugin or module) we can do a simple rename |
Yeah I know that feeling very well 😛. BTW I agree with you with the file/directory naming. My hesitation with this plug-in/module discussion comes mainly from the nature of the 'plug-ins' we can define in AFJ and the way they are registered and interact with other parts of the framework. When I think about 'plug-ins', the first thing it comes to my mind is a piece of code (script or library) that gives a multimedia player the ability of opening a file encoded in a certain format. Different plug-ins can coexist in an installation and have a common API to register and handle input/output data. In AFJ world we can do analogies in different aspects of the framework: a plug-in can be a module that supports a new protocol, or a module that gives support to a new credential format, or a new DID method (among other things). Does this 'plug-in API' satisfy these cases? Or would it be better to let each customizable module to handle its own plug-in API? I think it fits pretty well for modules that provide support to a new DIDComm protocol, or any side module that want to make use of some framework resources for a particular functionality (such as storing generic records in the wallet). But maybe it's not so evident for the other cases. And there is also an inevitable dependency between different modules registered with this API (e.g. some modules need services registered by others, such as ConnectionService), which definitely differ so much from the concept of 'plug-in' I'm used to. So, considering current state of the framework and this API, I think they are closer to 'pluggable modules' than to 'plug-ins', and therefore the term 'module' seems to me more appropriate. |
Great discussion. Words matter. I hesitate to comment but if all 'normal' words seem misleading, consider creating your own word that harkens to the meaning.... pluggables? mod-ins? modables? it's fun, forces others away from assumptions that are wrong. Admittedly some devs will hate it. |
I have a very minor nitpick and it is mainly to improve the register api. Right now, as defined in a comment above, it is like this: @module()
class MyModule {
public static register(dependencyManager: DependencyManager) {
dependencyManager.registerContextScoped(MyApi)
dependencyManager.registerSingleton(MyService)
dependencyManager.registerSingleton(MyRepository)
}
} While this is quite nice, I think that passing in the I propose to change the api to this: type Return = {
// TODO: these need way better names to make it more userfriendly and understandable
contextScoped: Token[]
singleton: Token[]
}
@module()
class MyModule {
public static register(): Return {
return {
contextScoped: [MyApi],
singleton: [MyRepository, MyService]
}
}
} This method hides most of the internals of what specific way you want to register it and IMO makes it a lot less complex. Under the hood ofcourse we still register it in the same way with the |
I'm not too sure on this. It loses some of the possiblities of the imperative way to do it. Tokens are not always the class itself, so we would need to tweak it to work with token => value pairs. So it would need to look something like this:
In addition I do think it sometimes is required to check on the dependency manager if something is already registered, but we may be able to work around that. |
I like your elaborate response @genaris and think I agree with your reasoning for why we should go with module over plugin. 👍
That might work, but I'm worried we risk it becoming even more ambiguous. What's a mod-in, and how would we communicate this clearly to users. But it may be more clear than to put our own meaning to the word module... |
Yeah okay that makes sense. Might be worth thinking about it bit later if we notice any complexity issues with it. For now I am perfectly okay with merging this :). |
I have merged the PR, but feel free to leave additional comments on how to better approach this. This unblocks a lot of other work for me so would like to keep moving. But we can make a final pass before releasing 0.3.0 and improve the api |
Adds an initial (mostly internal for now) plugin API that will be used as part of modularization. The changes from this PR are also needed for multi-tenancy.
I've not made breaking changes yet, so we can already get this merged and start releasing it, and we can do some clean up of the old custom module approach once we're ready to make the next breaking change release.
As this doesn't contain any breaking changes, I think we should merge this before #881 and #898
The biggest change in this PR is to move from a more implicit registration of services and modules, to a more explicit way to declare services and modules.
Instead of declarating a class with
@scoped(Lifecycle.ContainerScoped)
it now just needs@injectable
(which is exported from thesrc/plugins
directory. Then you can define a plugin to register modules and services. There's currently tow types of plugins, where a module plugin extends a default plugin.A module is declared like below:
For now all module classes declare the module itself, but as discussed previously the current module classes will be renamed to
xxxApi
, and a module will act as the combination off everything that module adds (so more than just the public api)It's not mean to be used publicly yet (and will have breaking changes), but you can then register the module on the dependency manager:
This will make sure the services are registered and in case of the module plugin the module is also registered. The reason why we need this for multitenancy is that we're going to create child container for each tenant agent, that share most of the services from the base agent, but will have some differences. The
@scoped(Lifecycle.ContainerScoped)
won't do the job anymore.I've exported some of the tsyringe methods and wrote a simple DependencyManager class to abstract away most of the
TSyringe
functionality so that you don't need to import from that dependench to write plugins, the needed primivities are exported from AFJ itself.