lightweight-sdk/spec/architecture.md
Nicolas 509bd3a537 Ajouter @lightweight-sdk/service-discovery (packages/server)
Agregateur d'API reverse-proxy avec enregistrement et decouverte de
services au runtime, deux points d'entree separes partageant un meme
protocole :

- ./client (DiscoveryClient) : enregistre un service et maintient un
  heartbeat periodique (qui sert aussi de mecanisme de retry - un echec
  est journalise et laisse au tick suivant, pas de logique separee).
  stop() est best-effort et ne rejette jamais.
- ./aggregator (ServiceAggregator) : handlers Node natifs a monter dans
  Express/Fastify/http brut (exemples des trois dans le README), zero
  dependance framework. Routage par prefixe de nom de service, TTL avec
  balayage periodique, secret partage compare en temps constant, proxy
  HTTP fait main (node:http/https, streaming, filtrage hop-by-hop,
  distinction 502/504).

Limites documentees dans le README plutot que corrigees : pas de
matching :param (v1, match exact), pas de secret par service, risque
SSRF-like assume vu le threat model homelab.

Resolution des sous-chemins via des stubs package.json (client/,
aggregator/) puisque le repo cible moduleResolution: Node10, pas de
champ exports moderne - pattern documente dans spec/architecture.md
pour de futurs packages server/ multi-points d'entree.

Bug trouve et corrige en cours de route : req.on('close') se declenche
des que le corps de la requete entrante est lu (immediat pour un GET
sans body), pas a la deconnexion reelle du client - detruisait ma
propre requete sortante juste apres l'avoir envoyee. Le bon signal est
res.on('close').

Correction connexe : create-package.mjs inserait la nouvelle ligne en
fin de fichier de spec/packages.md au lieu de l'inserer dans le
tableau - corrige pour les deux packages precedents au passage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 19:01:33 +02:00

58 lines
6.5 KiB
Markdown

# Architecture
## Objectif
`lightweight-sdk` est un monorepo pnpm destiné à héberger, au fil du temps, des packages TypeScript réutilisables développés par l'auteur pour ses projets personnels. Chaque package est publié indépendamment sur le registre npm privé hébergé sur l'instance Forgejo personnelle (`git.dev.kyuno.fr`), sous le scope `@lightweight-sdk`.
## Organisation des packages
Les packages sont répartis en trois catégories sous `packages/`, qui reflètent la distinction Frontend/Backend du [CLAUDE.md](../CLAUDE.md) du projet :
- **`client/`** — packages destinés au navigateur (DOM, composants UI). Peuvent dépendre de `lib: DOM` dans leur `tsconfig.json`.
- **`common/`** — code isomorphe partagé entre `client` et `server` (types, utilitaires purs, constantes). Convention : ne doit dépendre d'aucune API exclusivement DOM ou exclusivement Node, pour rester portable des deux côtés.
- **`server/`** — packages Node/backend. Chaque package doit être structuré comme un service modulaire autonome exposant sa propre API (règle "Architecture Backend" du CLAUDE.md).
Le nom npm d'un package ne reprend pas le préfixe de catégorie (`@lightweight-sdk/web-component`, pas `@lightweight-sdk/client-web-component`) — la catégorie est une convention d'organisation du dépôt, pas une convention de nommage public.
Un nouveau package se crée avec `pnpm create-package <client|common|server> <nom>` (voir `scripts/create-package.mjs`).
## Choix d'outillage
| Aspect | Choix | Pourquoi |
|---|---|---|
| Gestionnaire de packages | pnpm workspaces + `catalog:` | Centralise les versions des devDependencies partagées dans `pnpm-workspace.yaml` |
| Orchestrateur de build | Aucun (pas de Turborepo/Nx) | Un seul package réel au démarrage ; à reconsidérer si le temps de build devient un problème |
| Build | `tsc -b` (project references), pas de bundler | Les packages sont consommés via pnpm par d'autres projets Node/TS de l'auteur ; pas besoin de bundle |
| Modules | **CommonJS**, résolution `Node10` | Décision explicite : pas d'extension `.js` requise dans les imports relatifs, et le code migré depuis l'ancien repo `web-component` compile sans modification |
| Lint + format | [Biome.js](https://biomejs.dev/) | Imposé par le CLAUDE.md du projet |
| Documentation de code | [TypeDoc](https://typedoc.org/) en mode validation | Génère la doc API et fait échouer la CI si un export n'a pas de TSDoc (voir `conventions.md`) |
| Tests | [Vitest](https://vitest.dev/) | ESM natif, bon support monorepo |
| Versioning/publish | [Changesets](https://github.com/changesets/changesets) | Gère des versions indépendantes par package + changelogs, comprend nativement `workspace:*` |
| Node cible | 24 (Active LTS) en CI, `engines: ">=22"` | Node 20 est end-of-life depuis avril 2026 |
## Décorateurs et `useDefineForClassFields`
`tsconfig.base.json` force `"useDefineForClassFields": false`, bien que la cible soit `ES2022` (où ce flag vaut `true` par défaut). Sans ça, un champ de classe déclaré sans initialiseur (`logger!: LoggerService;`) est réinitialisé à `undefined` **sur l'instance** au moment de la construction, ce qui masque silencieusement tout accesseur posé sur le prototype par un décorateur de propriété (`@Service` de `@lightweight-sdk/service-registry`, `@LinkToDom` de `web-component`). C'est une interaction connue entre `useDefineForClassFields` et les décorateurs legacy (`experimentalDecorators`) — désactiver ce flag restaure la sémantique classique où un champ sans initialiseur n'émet aucun code, laissant l'accesseur du prototype pleinement fonctionner.
## Packages multi-points d'entrée
Un package peut exposer plusieurs points d'entrée séparés (ex. `@lightweight-sdk/service-discovery/client` et `.../aggregator`) quand son usage se scinde naturellement en deux surfaces qui ne doivent pas forcément être importées ensemble, tout en partageant un même protocole/types de base (une seule version à faire évoluer, pas de risque de dérive entre deux packages séparés). Convention établie avec `service-discovery`, à réutiliser pour de futurs packages `server/` dans le même cas :
- **Résolution sous CommonJS/`Node10`** : pas de champ `exports` moderne (ce mode de résolution ne le comprend pas). À la place, un répertoire stub à la racine du package par sous-chemin, contenant un mini `package.json` qui pointe vers le vrai fichier compilé — ex. `client/package.json` avec `{ "main": "../dist/client/index.js", "types": "../dist/client/index.d.ts" }`. Ces stubs doivent être listés dans `"files"` du `package.json` du package pour être effectivement publiés.
- **Un seul `tsconfig.json`** pour tout le package (pas un projet TS par point d'entrée) : `tsc -b` compile naturellement chaque `src/<sous-chemin>/index.ts` vers `dist/<sous-chemin>/index.js` en préservant l'arborescence.
- **Ré-exports nommés explicites obligatoires** dans chaque `index.ts` (jamais `export *`) : chaque point d'entrée doit ré-exporter les types du protocole partagé qu'il utilise dans sa surface publique, sinon la validation TypeDoc (bloquante en CI) échoue. `typedocOptions.entryPoints` dans le `package.json` du package doit lister les trois (ou plus) fichiers d'entrée.
## Pattern de handler framework-agnostique (packages `server/`)
Pour qu'un package `server/` reste utilisable quel que soit le framework HTTP de l'app hôte (le parc existant de l'auteur est déjà hétérogène : Express sur un projet, Fastify sur un autre), exposer la logique métier comme des handlers au format natif Node — `(req: IncomingMessage, res: ServerResponse) => void | Promise<void>` — plutôt que de dépendre d'Express, Fastify ou une lib de routing. L'app hôte les monte elle-même :
- **Express** : compatible direct, `req`/`res` Express *sont* des `IncomingMessage`/`ServerResponse`.
- **Fastify** : nécessite `reply.hijack()` avant d'appeler le handler avec `request.raw`/`reply.raw`, pour indiquer à Fastify de ne plus toucher `reply` ensuite.
- **`node:http` brut** : correspond exactement à la signature, aucun adaptateur nécessaire.
## Dépendances entre packages
Les dépendances internes entre packages du monorepo utilisent le protocole `workspace:*` (résolu localement par pnpm, jamais via le registre distant). Le registre npm privé n'est utilisé que pour la publication et pour la consommation depuis des projets *externes* à ce monorepo.
## Registre npm privé
`https://git.dev.kyuno.fr/api/packages/kyuno/npm/` — voir `ci-cd.md` pour le détail de la publication et de l'authentification.