Cordis et l'architecture de plugins en TypeScript sans prise de tête
Si vous avez déjà écrit une application extensible en Node.js ou TypeScript, vous avez probablement rencontré les mêmes écueils. Un utilisateur ou un système connecte un plugin. Le plugin accroche cinq écouteurs d'événements, démarre quelques minuteurs, enregistre ses routes et injecte un service. Et ensuite, le plugin est désactivé ou mis à jour à la volée.
Que se passe-t-il ensuite ? Exactement, des fuites de mémoire. Les écouteurs continuent de s'accrocher, les minuteurs continuent de tourner en arrière-plan, les références de contexte empêchent le ramasse-miettes de nettoyer la mémoire. Dans Node.js, la gestion du cycle de vie des dépendances se transforme souvent en travail manuel fastidieux.
Il y a quelque temps, les développeurs du framework de chatbot Koishi ont été confrontés exactement à ce problème. Ils avaient besoin de créer un cœur où des centaines de plugins tiers pourraient démarrer, s'isoler, se remplacer et se décharger sans redémarrer le processus. C'est ainsi qu'est né le framework Cordis.
Qu'est-ce que Cordis exactement
Les créateurs qualifient leur projet de « méta-framework de composabilité spatio-temporelle ». Cela paraît intelligent et prétentieux, mais l'essence est en fait très terre-à-terre.
Cordis combine un conteneur d'injection de dépendances (IoC), un bus d'événements et un arbre de contextes hiérarchique. Chaque plugin ou service réside dans son propre contexte. Si ce contexte est détruit, Cordis nettoie automatiquement absolument toutes les ressources pour lui : supprime les gestionnaires d'événements, arrête les minuteurs et supprime les services créés.
Il n'y a pas de magie ici, mais il y a une discipline claire : si un plugin utilise les méthodes [object Object], [object Object] ou [object Object], le framework se charge de nettoyer lui-même les effets secondaires.
Comment fonctionne le modèle de contexte
Le concept central de la bibliothèque est [object Object]. Ce n'est pas un simple objet plat avec des paramètres, mais un arbre ramifié.
Lorsque vous appelez [object Object], le framework crée un contexte enfant (fork). Le contexte enfant hérite des services du parent mais stocke ses propres références aux ressources enregistrées.
Si vous désactivez le Plugin A, son contexte enfant se ferme. Le gestionnaire [object Object] est supprimé du bus d'événements partagé, tandis que le Service de base de données et le Plugin B continuent de fonctionner normalement.
Services et typage en TypeScript
Les services dans Cordis sont déclarés par héritage de la classe de base [object Object]. Cela les rend accessibles directement via les propriétés du contexte tout en maintenant un typage strict :
La construction [object Object] résout le problème de l'ordre de chargement des modules. Si le service de base de données s'initialise de manière asynchrone ou se connecte plus tard, le plugin dépendant attendra sa disponibilité et s'activera lui-même.
Réglage fin de la visibilité des portées
Dans les programmes réels, les modules ne devraient souvent pas réagir à tout. Par exemple, un gestionnaire n'est nécessaire que pour les messages d'un canal spécifique ou les requêtes avec un en-tête particulier.
Cordis introduit le concept de filtres via l'appel [object Object] et les propriétés du contexte. Vous pouvez limiter la visibilité d'un service à une branche spécifique de l'arbre ou définir un prédicat qui filtre les événements indésirables :
Pour quelles tâches cette approche est-elle adaptée
La bibliothèque a été créée pour une classe spécifique d'applications. Vous ne devriez pas la traîner dans une API CRUD classique sur Fastify ou Express, où elle créerait une couche d'abstraction supplémentaire.
Mais Cordis s'intègre parfaitement dans les scénarios suivants :
- Utilitaires CLI modulaires et générateurs. Lorsque les utilisateurs peuvent livrer des paquets npm qui étendent les commandes ou les pipelines de construction.
- Applications de bureau sur Electron/Tauri. Pour organiser un système d'addons et des thèmes qui peuvent être activés et désactivés à la volée sans recharger la fenêtre.
- Bots et hubs d'intégration. Si un service communique avec une douzaine de plateformes différentes (Telegram, Discord, Slack), et que chaque adaptateur a besoin de vivre de manière isolée.
- Outils d'automatisation. Où les processus sont configurés par les utilisateurs de manière dynamique via une interface web ou des fichiers YAML.
Pièges et inconvénients
Il n'y a pas d'outils parfaits, et Cordis a beaucoup de nuances spécifiques :
- Courbe d'apprentissage raide. La documentation est écrite dans un langage sec avec une abondance de termes spécifiques. Pour comprendre les concepts de fusion de portées et d'effets secondaires, vous devrez lire attentivement le code source.
- API peu familière. Lier les services à l'objet contexte via le module de fusion de types de TypeScript peut être déroutant au début pour ceux habitués aux NestJS ou InversifyJS classiques avec leurs décorateurs.
- Lié à un modèle mental. Si l'architecture de votre projet n'implique pas de déchargement dynamique fréquent du code, les avantages du gestionnaire d'effets intégré sont annulés par la complexité du code.
Cela vaut-il la peine d'essayer
Cordis est un projet d'ingénierie intéressant axé sur la gestion du cycle de vie des composants. Il résout élégamment le problème des fuites de ressources lors de la création de systèmes extensibles.
Si vous concevez un système avec un écosystème développé de plugins enfichables et souhaitez un mécanisme fiable pour suivre les effets secondaires prêt à l'emploi, forkez le dépôt [object Object] et étudiez les exemples dans les tests. C'est un excellent exemple de la façon dont vous pouvez construire une architecture de micro-noyau en TypeScript pur.
Projets similaires