Aller au contenu

Migration et adoption ​

La règle de migration la plus sûre est la suivante : conservez le plan de données et adoptez la frontière que vous savez expliquer.

Gardez le framework Web, le worker, le scheduler, le broker, le logger ou le fournisseur de télémétrie existant. Ajoutez un contrat go-like explicite autour d'un vrai problème de cycle de vie ou d'appel de service. Vérifiez cette frontière avant d'ajouter un autre fournisseur.

Une migration par étapes ​

  1. Laissez le bootstrap existant et le code des routes/du plan de données inchangés.
  2. Identifiez un propriétaire : listener, worker, scheduler, souscription de broker, destination de logs ou fournisseur de télémétrie.
  3. Ajoutez un adaptateur Server structurel ou utilisez un adaptateur go-like existant. Définissez l'admission, l'arrêt, le timeout et l'observation terminale.
  4. Ajoutez @go-like/context aux vraies frontières d'annulation ou d'échéance. Transmettez-le comme premier argument de l'opération.
  5. Ajoutez liveness et readiness avec @go-like/health et @go-like/web/health.
  6. Ajoutez un appel interne unaire typé en utilisant @go-like/transport-memory dans les tests.
  7. Ne déplacez cet appel vers @go-like/transport-http ou @go-like/transport-http/node que lorsqu'un wire réel ou un host Node natif est nécessaire.
  8. Ajoutez Registry, Config, Store, Cache, Broker, logs, métriques ou traces, une capacité à la fois.
  9. Notez le fournisseur, le runtime, le propriétaire et le niveau de preuve de chaque nouvelle frontière.

Ne commencez pas par une réécriture du service entier. L'intérêt de petits contrats est de garder petite l'unité de migration.

Matrice de migration des frameworks ​

Système existantGardez natifAdoptez d'abordFrontière actuelle
NestJSModules, controllers, decorators, DI, interceptors, pipes, adapterUn Server structurel personnalisé autour de l'application existante ou une frontière interne Client/Server distincteAucun pont Nest go-like ni intégration DI automatique n'existe dans ce dépôt
FastifyRoutes, plugins, hooks, request/reply, listener natifUn wrapper de cycle de vie personnalisé ou un pont Fetch explicitement implémentéAucune conversion actuelle de request/reply Fastify vers un Handler go-like n'est démontrée
HonoRoutes, middleware, sous-applications, app.fetchnewNodeServer(app.fetch, ...), puis newApp(...)L'intégration Fetch native est démontrée dans examples/hono
ElysiaArbre de routes, schémas, decorators, derives, hooks, comportement Bun/Web Standardapp.fetch natif avec le host/cycle de vie Core lorsque c'est pertinentConservez la sémantique Bun de .listen() ; n'en faites pas une API go-like inter-runtime
H3Routeur H3 et conversion native du handlerLe chemin Fetch du handler de l'exemple H3 actuelapp.fetch de H3 2.x est la forme démontrée ; l'ancienne recommandation toWebHandler nécessite son propre exemple épinglé
KoaMiddleware et routeur externeUn wrapper du propriétaire ou un appel de service interne@go-like/web n'accepte pas l'objet Node request/reply de Koa sans pont applicatif
tRPCRouter, middleware de procédures, parseurs input/output, adaptateurLe cycle de vie Core autour du host ou une frontière de transport interne séparéeL'Endpoint go-like n'est pas un router de procédures tRPC

Exemple Hono ​

Voici la forme d'intégration démontrée :

ts
import { Hono } from "hono"
import { name, newApp, server } from "@go-like/core"
import { signal } from "@go-like/core/node"
import { newNodeServer, port } from "@go-like/web/node"

const web = new Hono().get("/users/:id", (c) => c.json({ id: c.req.param("id") }))

const app = newApp(name("users"), server(newNodeServer(web.fetch, port(3000))), signal())

await app.run()

L'exemple Hono actuel conserve à Hono la responsabilité des routes et transmet le handler Fetch natif au host Node. Il n'ajoute ni table de routes go-like ni paquet de pont spécifique à Hono.

Elysia et H3 ​

Appliquez la même frontière à un framework qui expose un handler Fetch standard :

text
framework route table
  -> framework native Fetch handler
  -> @go-like/web/node (when using the Node host)
  -> @go-like/core App

Vérifiez l'adaptateur de runtime du framework avant d'importer un sous-chemin Node. L'adaptateur Bun d'Elysia et son adaptateur Web Standard n'ont pas un comportement d'écoute identique. Les versions de H3 et leurs API de conversion des handlers nécessitent également un exemple épinglé. Ne déduisez pas une promesse pour toute combinaison de versions et de runtimes à partir d'un seul exemple.

Migrer un service Go ​

Pour un lecteur venant de Go ou de Kratos, migrez les concepts plutôt que les noms :

Concept GoConcept go-likeDifférence importante
context.Context@go-like/context Contextdone() est un AbortSignal ou null, pas un channel Go
Cycle de vie ServerServer structurel de Corestart(ctx) peut durer toute la vie du service et ne signifie pas readiness
App runnernewApp, App.run, App.stopApp.stop() n'accepte pas de Context du caller et renvoie une Promise partagée
RPC client@go-like/clientLes appels internes sont du JSON Fetch ou du SSE ; le retry est opt-in
Transport@go-like/transportLes fournisseurs et les headers Fetch sont des contrats TypeScript/Web
Registry@go-like/registryLes watchers renvoient des snapshots de remplacement complet
SelectornewRoundRobinSelector, newRandomSelector, newWeightedRoundRobinSelector, newP2CSelector, newEWMASelectorLe feedback est synchrone et propre à la policy
Protobuf/IDL@go-like/protoc-gen-likeCode Context-first sur Protobuf-ES
gRPC stream@go-like/transport-grpc-buf/nativegRPC standard avec quatre cardinalités via /native

Un premier pas incrémental consiste à effectuer un appel typé à une adresse directe sur Memory Transport :

ts
import { newClient, withEndpoint, withTransport } from "@go-like/client"
import { background, type Context } from "@go-like/context"
import { address, newServer, transport as serverTransport } from "@go-like/server"
import { struct } from "@go-like/struct"
import { defineService } from "@go-like/transport"
import { newMemoryTransport } from "@go-like/transport-memory"

const quoteService = defineService("pricing", {
  quote: {
    request: struct.object({ sku: struct.string() }),
    response: struct.object({ amount: struct.number() })
  }
})

const transport = newMemoryTransport()
const server = newServer(serverTransport(transport), address("memory://pricing"))
quoteService.registerHandler(server, {
  quote(_ctx, request) {
    return { amount: request.sku.length }
  }
})

const client = newClient(withTransport(transport), withEndpoint("memory://pricing"))
const caller = quoteService.newClient(client)
const ctx: Context = background()
const result = await caller.quote(ctx, { sku: "a" })
void result

Ce n'est qu'après avoir testé cette frontière qu'il faut introduire Discovery, un fournisseur Registry réel ou un transport HTTP. Vous conservez ainsi le contrat métier tout en remplaçant la destination et le câblage de propriété.

Adopter Kubernetes ​

Gardez Kubernetes natif :

  • Deployments, Services, DNS, Ingress, RBAC, probes, stratégie de rollout, HPA et network policy restent des responsabilités de la plateforme ;
  • @go-like/config-kubernetes lit une clé dans un ConfigMap ou Secret d'un seul namespace via une capacité Fetch injectée ;
  • @go-like/registry-kubernetes utilise les enregistrements EndpointSlice lorsqu'une découverte directe est réellement nécessaire ;
  • un EndpointSlice n'est pas le DNS d'un Kubernetes Service et ne fournit pas de TTL d'enregistrement universel ;
  • les références facultatives au propriétaire du Pod et le deregistration explicite ont des sémantiques d'échec différentes.

Commencez par la santé et la configuration avant de sélectionner directement depuis des EndpointSlice. Si l'application dispose déjà d'un nom DNS stable de Service, withEndpoint(...) avec un transport HTTP peut être plus simple et plus honnête que l'ajout d'un fournisseur Registry.

Adopter les brokers et les jobs ​

Conservez natifs le règlement et la policy des jobs :

Plan de données existantÀ garderAjoutez go-like pour
NATS CoreConnection, subscription, queue group, Msg, drainnewNatsCoreServer, newNatsCoreBroker, cycle de vie et frontière de bytes
NATS JetStreamStream, durable consumer, JsMsg, ack/nak/term, redelivery, DLQnewNatsJetStreamServer, newNatsJetStreamBroker, cycle de vie
RabbitMQConnection, topology, confirm policy, channelCycle de vie du subscriber borrowed ou recovering et règlement natif sûr par génération
BullMQQueue, Worker, processor, retry/backoff, RedisnewBullMqWorkerServer autour d'un Worker officiel dormant
CronerCron expression, time zone, callback, overlap policynewCronerServer autour de jobs Cron natifs en pause
Memory BrokerMap de topics en processus et sémantique de testnewBrokerServer et un codec d'événement facultatif

Ne migrez pas ack/nak/term de NATS, le règlement durable de JetStream, les confirmations RabbitMQ ou les retries BullMQ dans une abstraction générique go-like Broker. Ces sémantiques expliquent précisément pourquoi l'objet natif du fournisseur reste visible.

Migrer l'état ​

Choisissez un domaine d'état à la fois :

  • Config pour les snapshots immuables de configuration du processus et le reload ;
  • Registry pour la joignabilité éphémère des services ;
  • Store pour les enregistrements faisant autorité, les révisions, CAS, TTL et pages ;
  • Cache pour les valeurs jetables qui peuvent être recalculées.

Un test de migration utile consiste à noter ce qui se passe après un redémarrage du processus, une lecture obsolète, une panne du fournisseur, une compaction du watcher, un conflit CAS et un cache miss. Si la réponse diffère, ces domaines ne devraient pas partager une interface générique de repository.

Ajouter l'observabilité ​

Ajoutez d'abord le fournisseur natif, puis enveloppez la frontière :

text
application creates logger / Registry / MeterProvider / TracerProvider
  -> go-like wrapper records bounded operation facts
  -> application-owned exporter or destination
  -> explicit Core lifecycle adapter closes the admitted resource

@go-like/prometheus n'utilise pas le registre global. @go-like/otel n'installe ni fournisseurs ni exporters globaux. Les adaptateurs Pino et Winston ne remplacent pas la configuration native du logger. Gardez les labels et attributs bornés et masquez séparément les logs appartenant à l'application.

Checklist d'acceptation de la migration ​

Avant de fusionner une frontière, vérifiez :

  • un propriétaire clairement nommé existe ;
  • le propriétaire reçoit le bon Context et ne le remplace pas par background() ;
  • l'admission au démarrage et la readiness sont distinctes ;
  • le comportement du timeout d'arrêt est documenté comme une limite d'attente ;
  • l'observation terminale native est conservée lorsqu'elle existe ;
  • les handlers Web externes et les handlers unaires internes ne sont pas mélangés ;
  • l'autorisation de retry correspond à l'opération métier ;
  • les credentials, metadata, logs et attributs de trace ont une policy de masquage ;
  • les sémantiques propres au fournisseur restent visibles ;
  • la commande unit/typecheck ciblée a réussi dans le checkout visé ;
  • la commande E2E pertinente (runtime, fournisseur, publication ou exemple) a été exécutée et consignée, ou explicitement marquée comme non exécutée.

Frontière de support actuelle ​

@go-like/protoc-gen-like génère le code Protobuf RPC avec Context en premier argument, sur Protobuf-ES. @go-like/transport-grpc-buf fournit unary et server-streaming Connect/gRPC-Web via Fetch ; /native ajoute gRPC standard avec les quatre cardinalités, dont client-streaming et bidi. Ce chemin est indépendant du chemin interne Fetch/SSE.

Cela n’inclut pas gRPC standard dans le navigateur, request-streaming/bidi via Fetch, health/reflection officiels, authentification générique, Event Store/replay, ORM ou orchestration de clusters.

gRPC géré sous Node 26.12.1 et Bun 1.4.4 a passé le test de régression du drain avec consommateur lent. La correction des limites historiques d’annulation et d’arrêt de Connect 2.1.2 et de Bun 1.4.2 Fetch reste non vérifiée.