-
-
Notifications
You must be signed in to change notification settings - Fork 132
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
Dynamic extensions reloading: IPlugin.deactivate
, Application.registerPlugin
and Application.deactivatePlugin
proposal
#278
Comments
IPlugin.deactivate
proposalIPlugin.deactivate
, Application.registerPlugin
and Application.deactivatePlugin
proposal
I extended the proposal to |
Overall, I think this is an excellent idea and would love to see it in Lumino/JLab. Thanks for proposing it!
Yes, there was the assumption in JupyterLab that reload was at least cheap enough to not justify the effort at the time. Thanks for showing that in certain cases it would be very valuable.
Should unregisterPlugin also call deactivate if the plugin is activated? I think so, so I'm curious why you didn't choose that.
There is a topological sort in Lumino that it uses for activation that we could use.
If the unregister is calling deactivate, perhaps that is part of what the force option would do - if force is false, it would throw the error, and if force was true, it would still unregister the plugin, possibly leaving your system in an inconsistent state.
+1 Also, we should check that the following scenario works:
Does plugin A get activated again automatically by the system, with the new B? What happens in the four combinations of auto-activation settings for plugin A and plugin B? |
It was my initial thought too but I decided not to include deactivate in unregister there for two reasons:
Right, under the revised proposal this question is easier to answer because
I think that under the current proposal this will not happen.
This is a good question; again I don't think it is a problem when
I think the answer is (2) as it would make this method more useful and enable better resource management for the extension developers (if they spawn any external resources, etc - they will no longer need to rely on window events like |
While I do think that it is worth thinking through the dependency tree of deactivations etc., a first step might be to only allow for the deactivation (and unregistering?) of plugins that are not depended upon by other plugins. I.e. only leaf nodes in the dependency tree. We can future-proof it by documenting that "if you try to deactivate a plugin that is depended on by another plugin, this will currently throw an exception, but this might change in the future to allow deactivation of a plugin and all of its dependents, so do not rely on this feature." |
In addition to this it could also be useful to allow for more app introspection, for example to check if a plugin is activated (and has not failed to activate), which could be exposed via a |
What about using a generator for the activate function that returns [Token?, deactivate function?] instead of a separate The idea behind using a generator is linked to the actions the deactivate method will need to carry; aka mainly disposing created resources and disconnecting signals. using separated const plugin = {
provides: IMyToken,
activate: (app): IMyToken => {
const john = new MyAwesomeDisposable();
const onAppRestored = () => {
// no-op
john.initialize()
}
app.restored.connect(onAppRestored);
return john;
}
deactivate: () => {
// How to disconnect the signal
// How to dispose john
// => the solution will probably to create a class implementing IPlugin rather than a dictionary
}
} using generator const plugin = {
provides: IMyToken,
activate: *(app): Iterable<IMyToken | (() => void)> => {
const john = new MyAwesomeDisposable();
const onAppRestored = () => {
// no-op
john.initialize()
}
app.restored.connect(onAppRestored);
yield john;
const deactivate = () => {
app.restored.disconnect(onAppRestored);
john.dispose();
}
yield deactivate;
}
} |
Indeed I was thinking extension equals a class. The generator would be another pattern on top of In the use-case of plugin-playground (which in my mind is meant to be a good first experience for powerusers who may not have had an experience with TypeScript) having deactivate in every example is essential, so using it would now also require learning about generators in TS. I also somewhat dislike having the sequence of yields which has to be done in a very specific order. What we could do instead is: class MyAwesomeDisposableExtension implements IMyToken {
constructor (restored: ISignal) {
this._restored = restored;
this._restored.connect(this._onAppRestored, this);
}
doX() {
// no-op
}
dispose() {
this._restored.disconnect(this._onAppRestored, this);
}
private _onAppRestored(): {
this._initialize()
}
private _initialize() {
// no-op
}
}
const plugin = {
provides: IMyToken,
requires: [ISomethingRegistry],
activate: (app, somethingRegistry: ISomethingRegistry): IMyToken => {
const john = new MyAwesomeDisposableExtension(app.restored);
somethingRegistry.register('john-does-x', john.doX)
return john;
},
// what if `provides` is empty? give a null here? or should it be the first argument?
deactivate: (app, john: IMyToken, somethingRegistry: ISomethingRegistry) => {
somethingRegistry.unregister('john-does-x');
john.dispose();
}
} Note that in the generators example, john being a disposable holds state anyways (so my example could possibly be rewritten to a set of closures). |
As you commented, // what if `provides` is empty? give a null here? or should it be the first argument?
deactivate: (app, john: IMyToken, somethingRegistry: ISomethingRegistry) the trouble is that it will be hard to know what to provide as input to deactivate. I would rather prefer a plugin class pattern: const plugin = new (class extends JupyterFrontEndPlugin<IMyToken> {
readonly provides = IMyToken,
readonly requires = [ISomethingRegistry],
activate(app, somethingRegistry: ISomethingRegistry): IMyToken {
this._john = new MyAwesomeDisposableExtension();
app.restored.connect(onAppRestored, this);
return john;
}
deactivate(app, somethingRegistry: ISomethingRegistry){
app.restored.disconnect(onAppRestored, this);
this._john.dispose();
this._john = null;
}
onAppRestored() {
this._john.initialize()
}
private _john: MyAwesomeDisposableExtension | null = null;
})(); |
Problem
The dynamic extensions provided by
jupyterlab-plugin-playground
cannot be registered more than once with the same id. This forces the user to reload the page every time, or at least change the id each time they activate the plugin, which is really annoying for the prototyping workflows as intended for the plugin playground.Neither normal JupyterLab/Lumino extensions can be unloaded on runtime. It might be by design - maybe there was an assumption that reloading websites is cheap - but the cost is suboptimal for some more recent use cases like:
To allow to reload extension with new updated code we need to unload it first. We could remove the plugin manually from the plugin map (inelegantly accessing private properties of
Application
, which is what is currently done in jupyterlab/jupyterlab-plugin-playground#28) but when we register module anew, the old one remains active, with all of its commands, widgets, toolbars and menus attached.Proposed Solution
There should be three additions to the API are needed:
Application.unregisterPlugin()
method to remove the plugin from the plugin map and complement the existingregisterPlugin()
deactivate
method which plugins may decide to implement. If it is provided, it should undo all the changes theactivate
method made, including removing the added commands, menus, etc. to complement the currentactivate()
method.Application.deactivatePlugin
method to call thedeactivate
method on the plugin and all of it dependantsThe
unregisterPlugin
would roughly be:deactivatePlugin
would reject if plugin does not havedeactivate()
method:It will be up to plugin authors to adopt the
deactivate
method. This would be a backwards-compatible change.The signature of
deactivate
could be:Because plugins are also service providers we would want to unregister the associated service after the
deactivate()
call completed (allowing the plugin to cleanly shut the service down to avoid memory leaks), thus we want to allow the plugins to return a promise.If
deactivate
is not present the service provided by the plugin should never be removed.Questions:
void
because we would know what the associated service is; any other takes?deactivate()
is present but throws or rejects? Should we still unregister the service?Additional context
activate
anddeactivate
functions as entry points. Thedeactivate()
method is optional in VSCode.load
/unload
events for its dynamic extensions; if I read the documentation correctly it also disallows use of any entry points which cannot by loaded/unloaded dynamically for dynamic pluginsAdding a
deactivate
which enables hot reloading of dynamic extensions is a pre-requisite for using plugin-playground (also cf jupyterlab/jupyterlab#8866) to develop extensions inside JupyterLab itself (jupyterlab/jupyterlab#7469).Finally, deregistration wrapper will also need to be added downstream in JupyterLab where
registerPluginModule
is implemented.The text was updated successfully, but these errors were encountered: