Plugin API's design


Recently I have been looking into this issue and figured out that the fix is as easy as adding an element into an array. However, the plugin API does not provide the desired hooks yet. I thought that I could add them myself and finally get done with it.

Implementing these kind of hooks roughly means (considering how most of the existing hooks have been implemented):

  • In the hooked module: declare an array that initially holds the original values and will hold plugged values.
  • In the hooked module: declare a function that simply adds an element passed as parameter to the before declared array.
  • In the plugin API: add a function that calls the previously declared function from the hooked module.

Noticing that most of the hooks do this in one way or another, I continued my thought process and found a few limitations of these implementations:

  • First of all, one can only add and usually at the end of the list. The plugin developer has no control over the existent items (cannot replace, reorder, remove, etc.) unless there are functions exactly for this purpose (not only addSomethingToHook(), but also removeSomethingFromHook(), etc.).
  • There is plenty of duplicated code - all the code for this kind of hooks is more or less the same. It would have been fine if there were not that many hooks, but Discourse is rapidly expanding and this may lead to a mess in the future.
  • Some hooks have so few use cases and are simply not worth all this effort. For example, I have a hard time thinking of reasons why one would want to hook the available sorting methods of a category.

Then, as an exercise I started designing a new plugin API that would help with these issues. My idea is pretty simple: let the plugin developer register data manipulation functions for a specific key, which will be called by the hooked module through the plugin API.

For example, this is how one would implement the previous fix with this design.

  • The hooked function suffers minimal changes and becomes like this (just the pluginApply function call has been added):
availableSorts() {
  return pluginApply('categorySettings:availableSorts', [
    .map(s => ({ name: I18n.t("category.sort_options." + s), value: s }))
    .sort((a, b) => {
      return >;
  • The plugin developers register a function that appends one element to the array as:
withPluginApi('0.8.4', api => {
  api.register('categorySettings:availableSorts', availableSorts => {
    return availableSorts;
  • The pluginApply function takes all registered functions and calls them in order. Suppose that all plugins register f, g and h, pluginApply(key, initial) will call h(g(f(initial))).

I believe this design is much more flexible, could provide a better interface for plugin developers and would also keep the core codebase clean. One downside of this is that plugin developers may have too much control, but is this really an issue?

Do you think it would be worth implementing this into Discourse?

Anyway, this was more of a thought exercise and I just wanted to hear your opinions about this. Thanks! :smiley: