dimanche 16 octobre 2022

Creating a plugin system in React/Node

TL;DR

I want to build a plugin/extension system for a React/Node.js app similar to what VS Code or Jira has. The plugins can be created by users and need to run in a sandbox environment inside of the main React app while having access to some API's provided by this main app. I did a lot of research and haven't been able to find useful information. I'm not necessarily looking for a complete solution or even a partial solution. Some insights, tips or any advice would be greatly appreciated if you have experience working with or building such architectures.

The problem

Note: I know my problem is probably overengineering a simple solution, but my goal is to build a robust and stupidly extensible ecosystem for a stupidly simple task (and learning in the process). It's not about finding the easiest and fastest solution, it's about sending a message.

So, my V1 application was a simple scoreboard controller for handball matches, it worked just fine in production for over a year now, but we've made some questionable design decisions early on and modifying a simple thing became a pain, let alone adding a whole new feature. That's why we've decided to start over from scratch and create a whole new app with more capabilities, including support for multiple sports. I know from the beginning that maintaining everything in a single codebase would lead to the same roadblocks we faced with V1. The first idea was to make a monorepo, where every sport was its own library extending the same core library and plugging into the main application. This would be fine, but I want to have more control over how the individual pieces connect and give the ability to other developers to extend the system (very unlikely that anyone would ever want to do it, but hey, the option would be nice).

Also, these plugins should be accessible from a "marketplace", kinda like Atlassian's apps. I've worked on a Jira extension at my job and really liked how it fit into their ecosystem. I've yet to try to "reverse-engineer" it or look into how Atlassian integrates these plugins into their system but since that's closed-source I don't think that would get me very far. I know in the frontend it's using an iframe to display the app inside Jira, which is a valid option I'm considering too.

One proposed solution overcomplication

Please skip to the App structure section to know more about the actual architecture. I know, I know, I should've started with that (or never written this but it's already too late)

My first idea was to have a few building blocks built into the app and have the game logic be stored in a JSON/YAML format. That would make storing the extensions in a database trivial and everything could be served from our backend. This file would contain definitions like what values a game has (scores, penalties, timeouts etc) what events are sent when some conditions are met, how the controllers and displays are constructed and where and how these values are connected. An interpreter would then extract the logic and run it on the corresponding locations: build the HTML template for the display and the controller and start the game with the timers and events on the backend magically connecting them. As you can see this is a very complicated way of solving the problem and the more I work on it the more I realize it's either going to become something like a programming language itself or I have to compromise on the extensibility which was the whole point in the first place. I'm already questioning my choices in life at this point.

What I'm dreaming of

I found this project react-pluggable the other day and it was everything I've had in mind. With one exception. Of course, the React modules have to be imported which defeats the whole purpose of disconnecting the plugins and storing them in a database or hosting them on a different server. I wish there was a solution to fetch components over the internet, dynamically, in runtime. And preferably cache them in case there is no internet connection which is apparently a required use case (more on that later).

App structure

I've ranted enough about what I want to achieve, now I want to give some context how the application is structured. The CLIENT is an Electron application running locally. It talks to a Node.js the server through ws but that's not always an option so it has to work independently and we shouldn't worry about it now. The CONTROLLER is a React app that will control the DISPLAY which is displayed by the CLIENT. The CONTROLLER sends events to the CLIENT like "hey, the home team scored a goal", and the CLIENT updates the game state and sends an event to DISPLAY like "hey, you should be displaying 1-0". Also, the clock complicates things because it has to be synced with everything. The CLIENT can also send events to the DISPLAY like "hey, this player's 2 minute penalty is over, you should update accordingly". So the CLIENT is always the source of truth in the system.

The information flows DISPLAY <- CLIENT <-> CONTROLLER. And this is where the whole plugin thing comes into the picture. The CLIENT doesn't know what to do except the basic functions like "start the clock", "update the clock", "wait until the timeout is finished" and "update the DISPLAY's DOM somewhere". and forwarding messages back and forth. The CONTROLLER doesn't know how to run a game, it only knows "there should be a button saying HOME +1 that sends a home goal event". And the DISPLAY is even more clueless, it only knows "monkey receive update to DOM, monkey updates the DOM".

All this magic in connecting the different parts of the application should be encapsulated in a single package (preferably, or in 3 submodules for the different parts) that could be fetched at runtime.

Final thoughts

Huh, that was a lot, I hope you're still with me. If you have any input on how this problem could be solved/simplified or have any ideas on how you would approach it, please share it with me. And if there's any more detail I could add or edits I could make to clarify, please let me know.

Aucun commentaire:

Enregistrer un commentaire