@nodefony/http 10.0.0-beta.6 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -2
- package/dist/nodefony/config/config.js +25 -2
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +41 -9
- package/dist/nodefony/service/http-kernel.js +198 -19
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +1 -1
- package/dist/nodefony/service/servers/clientError.js +26 -1
- package/dist/nodefony/service/servers/server-h2c.js +3 -3
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +4 -4
- package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/servers/serverShutdown.js +14 -36
- package/dist/nodefony/service/sessions/sessions-service.js +6 -6
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +9 -4
- package/dist/nodefony/src/context/http/HttpContext.js +86 -8
- package/dist/nodefony/src/context/http/Request.js +96 -16
- package/dist/nodefony/src/context/http/Response.js +120 -17
- package/dist/nodefony/src/context/http/SseStream.js +4 -4
- package/dist/nodefony/src/context/http/StreamLifecycle.js +328 -19
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Response.js +47 -8
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/websocket/Response.js +11 -1
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +15 -4
- package/dist/nodefony/src/cookies/cookie.js +3 -1
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +1 -1
- package/dist/nodefony/src/proxy/generateProxyConfig.js +91 -15
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/nodefony/config/config.d.ts +10 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +22 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +21 -1
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +2 -2
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +56 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +38 -8
- package/dist/types/nodefony/src/context/http/Response.d.ts +61 -7
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +35 -2
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +8 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +7 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/docs/byte-stream.md +52 -17
- package/docs/diagnostics-channel.md +45 -1
- package/docs/servers.md +57 -44
- package/docs/upload.md +43 -17
- package/package.json +26 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// \0@oxc-project+runtime@0.
|
|
1
|
+
// \0@oxc-project+runtime@0.158.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
package/dist/index.js
CHANGED
|
@@ -33,12 +33,13 @@ import HttpContext from "./nodefony/src/context/http/HttpContext.js";
|
|
|
33
33
|
import WebsocketContext from "./nodefony/src/context/websocket/WebsocketContext.js";
|
|
34
34
|
import HttpRequest from "./nodefony/src/context/http/Request.js";
|
|
35
35
|
import Http2Request from "./nodefony/src/context/http2/Request.js";
|
|
36
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
36
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
37
37
|
import { parseRequestTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
38
38
|
import { decideSend, readBackpressureOptions } from "./nodefony/src/context/websocket/wsBackpressure.js";
|
|
39
39
|
import { FRAME_RATE_WINDOW_MS, admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter } from "./nodefony/src/context/websocket/wsFrameRate.js";
|
|
40
40
|
import { isWebsocketContext } from "./nodefony/src/context/websocket/isWebsocketContext.js";
|
|
41
41
|
import { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
|
|
42
|
+
import { rpcCorsLists } from "./nodefony/src/grpc/cors.js";
|
|
42
43
|
import { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "./nodefony/src/context/domainMatcher.js";
|
|
43
44
|
import { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
|
|
44
45
|
import { SSE_HEARTBEAT_MS, SseStream, acceptsEventStream, openSseStream } from "./nodefony/src/context/http/SseStream.js";
|
|
@@ -119,4 +120,4 @@ Http = __decorate([services([
|
|
|
119
120
|
upload_service_default
|
|
120
121
|
])], Http);
|
|
121
122
|
var http_default = Http;
|
|
122
|
-
export { ACTION_SELF_PHASE, Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FRAME_RATE_WINDOW_MS, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, PrebuiltUi, PrettyRequestLogger, Profiler, REQUEST_TRACE_CHANNEL, ReverseProxy, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, SSE_HEARTBEAT_MS, Session, sessions_service_default as SessionsService, SseStream, TOOLING_URL_PREFIX, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, acceptsEventStream, admitFrame, aggregateProfiles, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, createProfileAggregator, decideSend, http_default as default, defineHttpConfig, frameRateRejectedTotal, httpConfigJsonSchema, httpConfigSchema, isContentlessStatus, isDomainAllowed, isFailedProfile, isWebsocketContext, normalizePrefix, openByteStream, openSseStream, parseProxyOrigin, parseRequestTarget, proxyMountProblems, readBackpressureOptions, readFrameRateBound, readFrameRateCloseAfter, resolveUiDelivery, schemaMismatchOf, sqlShape, toValidationFields };
|
|
123
|
+
export { ACTION_SELF_PHASE, Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FRAME_RATE_WINDOW_MS, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, PrebuiltUi, PrettyRequestLogger, Profiler, REQUEST_TRACE_CHANNEL, ReverseProxy, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, SSE_HEARTBEAT_MS, Session, sessions_service_default as SessionsService, SseStream, TOOLING_URL_PREFIX, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, acceptsEventStream, admitFrame, aggregateProfiles, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, createProfileAggregator, decideSend, http_default as default, defineHttpConfig, frameRateRejectedTotal, httpConfigJsonSchema, httpConfigSchema, isContentlessStatus, isDomainAllowed, isFailedProfile, isWebsocketContext, normalizePrefix, openByteStream, openSseStream, parseProxyOrigin, parseRequestTarget, proxyMountProblems, readBackpressureOptions, readFrameRateBound, readFrameRateCloseAfter, resolveUiDelivery, rpcCorsLists, schemaMismatchOf, sqlShape, toValidationFields };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizePrefix, proxyMountProblems } from "../src/proxy/rules.js";
|
|
2
|
+
import { GRPC_DEFAULT_MAX_MESSAGE_BYTES, GRPC_MAX_MESSAGE_BYTES } from "../src/grpc/limits.js";
|
|
2
3
|
import { overlayField } from "nodefony";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
// nodefony/config/config.ts
|
|
@@ -146,8 +147,8 @@ const httpServerSchema = z.looseObject({
|
|
|
146
147
|
maxHeadersCount: z.number().int().nonnegative().default(2e3).describe("Nombre max d'en-têtes par requête — anti header flooding."),
|
|
147
148
|
keepAliveTimeout: z.number().int().nonnegative().default(5e3).describe("Délai keep-alive (ms) entre deux requêtes sur la même socket TCP."),
|
|
148
149
|
timeout: z.number().int().nonnegative().default(3e4).describe("Timeout global de la socket (ms). 0 = désactivé. Aligné sur responseTimeout : node arme ce timer à la connexion, et le framework ne ré-arme que si la valeur diffère (`socket.timeout !== ms`) — l'alignement économise 1 setTimeout par requête sans changer le comportement effectif (l'arm framework 30 s dominait déjà)."),
|
|
149
|
-
requestTimeout: z.number().int().nonnegative().default(3e4).describe("Timeout de réception requête complète (ms) — anti slow-loris."),
|
|
150
|
-
responseTimeout: z.number().int().nonnegative().default(3e4).describe("
|
|
150
|
+
requestTimeout: z.number().int().nonnegative().default(3e4).describe("Timeout de réception requête complète (ms) — anti slow-loris. HTTP/1.1 : borne native de Node ; HTTP/2 : borne du framework (corps lu par le pipeline), 408 puis flux fermé. 0 désactive."),
|
|
151
|
+
responseTimeout: z.number().int().nonnegative().default(3e4).describe("Délai d'inactivité d'une action (ms) : au-delà, 503 et `ctx.signal` annulé. Une route le règle pour elle-même (option `responseTimeout`)."),
|
|
151
152
|
shutdownTimeout: z.number().int().nonnegative().default(5e3).describe("Drain graceful au shutdown (ms) : délai laissé aux requêtes in-flight avant destruction forcée des sockets (SIGTERM/docker stop). Doit rester < grace period de l'orchestrateur (30 s k8s, 10 s Docker)."),
|
|
152
153
|
headers: z.record(z.string(), z.string()).nullable().default(null).describe("En-têtes ajoutés à toutes les réponses. `null` = aucun.")
|
|
153
154
|
}).describe("Serveur HTTP (node:http, port 5151). Loose : toute option `net.Server`/`http.Server` supplémentaire (insecureHTTPParser, maxRequestsPerSocket…) est transmise.");
|
|
@@ -221,6 +222,7 @@ const websocketSchema = z.looseObject({
|
|
|
221
222
|
})]).default(false).describe("Compression permessage-deflate (RFC 7692). Défaut false : la compression coûte CPU + mémoire et expose au zip-bomb (un petit payload compressé se décompresse en message géant). `true` = options par défaut, ou objet pour régler fenêtres / seuil / concurrence. N'activer que si le gain réseau le justifie."),
|
|
222
223
|
skipUTF8Validation: z.boolean().default(false).describe("Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). Gain CPU au prix de l'acceptation de frames texte invalides — laisser false sauf charge extrême maîtrisée."),
|
|
223
224
|
autoPong: z.boolean().default(true).describe("Répond automatiquement par un pong à chaque ping entrant (RFC 6455 §5.5.2-5.5.3). Laisser true — requis par le protocole et par le heartbeat des pairs."),
|
|
225
|
+
maxPendingAutoPongs: z.number().int().nonnegative().default(60).describe("Pongs automatiques en attente d'écriture au-delà desquels la connexion est COUPÉE (ws@8.23+). Borne la mémoire face à un pair qui inonde de pings sans jamais lire : chaque ping met un pong en file, et la file d'un pair qui ne lit pas grossit jusqu'à l'épuisement du process. Un pair sain n'a jamais plus de quelques pongs en vol. 0 = sans borne (vulnérable). Défaut 60, celui de `ws`."),
|
|
224
226
|
allowSynchronousEvents: z.boolean().default(true).describe("Émet les events ('message', 'ping'…) de plusieurs frames lues dans un même chunk réseau de façon synchrone (ws@8.18+). true = meilleur débit ; false = un event par tick d'event loop (latence plus régulière, plus équitable entre connexions)."),
|
|
225
227
|
maxBackpressure: z.number().int().nonnegative().default(4194304).describe("Seuil (octets) du buffer d'envoi par connexion (`ws.bufferedAmount`) au-delà duquel `backpressurePolicy` s'applique. Borne la RAM d'envoi face à un client LENT À RECEVOIR (anti-OOM) ; broadcast() amplifie (un seul lent peut plomber la diffusion). 0 = désactivé. Défaut 4 MiB."),
|
|
226
228
|
backpressureCloseAfterDrops: z.number().int().nonnegative().default(1e3).describe("SOLDE de frames refusées au-delà duquel la connexion est FERMÉE (1013), en politique 'drop' : +1 par frame refusée, −1 par frame envoyée. POURQUOI un solde et pas un second seuil d'octets : une fois qu'on jette, plus rien n'alimente la file — elle plafonne au seuil de drop et n'atteint jamais un seuil supérieur, qui serait donc inatteignable (mesuré). Et pas une série remise à zéro au premier envoi : la file d'un client bloqué oscille autour du seuil, chaque envoi effacerait la série et il ne serait jamais fermé. Le solde, lui, distingue le pic passager du client mort. 0 = ne jamais fermer. Défaut 1000."),
|
|
@@ -329,6 +331,21 @@ const proxySchema = z.strictObject({
|
|
|
329
331
|
});
|
|
330
332
|
}
|
|
331
333
|
}).describe("Proxy inverse : relaie un préfixe d'URL (requêtes et, sur demande, WebSocket) vers un autre serveur, sur la MÊME origine que la page — un seul certificat, l'amont peut rester sur la boucle locale. En-têtes de connexion jamais transmis, `X-Forwarded-*` et `Via` posés, `trustedHosts` appliqué, 502/504 sur amont injoignable/muet, boucle détectée (508).");
|
|
334
|
+
/**
|
|
335
|
+
* Section `grpc` — ce que servent les points d'entrée gRPC d'une application
|
|
336
|
+
* (ADR-0026 D3, point 5). Lue au premier appel d'un service : une application
|
|
337
|
+
* qui n'en déclare aucun ne la lit jamais.
|
|
338
|
+
*
|
|
339
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
340
|
+
*/
|
|
341
|
+
const grpcSchema = z.strictObject({
|
|
342
|
+
grpc: z.boolean().default(true).describe("Sert le protocole gRPC (HTTP/2 seulement : 5152, et 5153 si h2c est activé). Défaut de l'adaptateur Connect."),
|
|
343
|
+
grpcWeb: z.boolean().default(true).describe("Sert le protocole gRPC-Web, sur tous les serveurs actifs."),
|
|
344
|
+
connect: z.boolean().default(true).describe("Sert le protocole Connect (JSON ou binaire, `curl` compris), sur tous les serveurs actifs."),
|
|
345
|
+
readMaxBytes: z.number().int().min(1).max(GRPC_MAX_MESSAGE_BYTES).default(GRPC_DEFAULT_MAX_MESSAGE_BYTES).describe("Taille maximale d'un message REÇU, en octets (défaut 4 Mio), après décompression : au-delà, statut 8 `RESOURCE_EXHAUSTED` et la méthode n'est pas appelée. Sans borne, quelques Ko compressés peuvent se décompresser en plusieurs Gio."),
|
|
346
|
+
writeMaxBytes: z.number().int().min(1).max(GRPC_MAX_MESSAGE_BYTES).default(GRPC_DEFAULT_MAX_MESSAGE_BYTES).describe("Taille maximale d'un message ÉMIS, en octets (défaut 4 Mio) : au-delà, statut 8 `RESOURCE_EXHAUSTED`, journalisé chez nous plutôt que refusé par un client borné à 4 Mio."),
|
|
347
|
+
requireConnectProtocolHeader: z.boolean().default(false).describe("Exige l'en-tête `Connect-Protocol-Version` sur les requêtes Connect (400 sans lui). Défaut `false` : le CSRF est déjà fermé par le type de contenu et CORS, et l'exiger casse `curl` et le JSON écrit à la main.")
|
|
348
|
+
}).refine((grpc) => grpc.grpc || grpc.grpcWeb || grpc.connect, { message: "grpc : au moins un protocole doit rester servi (grpc, grpcWeb ou connect) — pour ne servir aucun service gRPC, n'en déclarer aucun." }).describe("Services gRPC déclarés par `@GrpcService` (ADR-0026) : protocoles servis et bornes des messages, pour toute l'application. Expérimental.");
|
|
332
349
|
const httpConfigSchema = z.strictObject({
|
|
333
350
|
headerServer: z.string().nullable().default("nodefony").meta({
|
|
334
351
|
runtimeMutable: true,
|
|
@@ -366,6 +383,12 @@ const httpConfigSchema = z.strictObject({
|
|
|
366
383
|
rateLimit: rateLimitSchema.default(() => rateLimitSchema.parse({})),
|
|
367
384
|
health: healthSchema.default(() => healthSchema.parse({})),
|
|
368
385
|
proxy: proxySchema.default(() => proxySchema.parse({})),
|
|
386
|
+
/**
|
|
387
|
+
* Services gRPC servis par le pipeline (ADR-0026).
|
|
388
|
+
*
|
|
389
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
390
|
+
*/
|
|
391
|
+
grpc: grpcSchema.default(() => grpcSchema.parse({})),
|
|
369
392
|
wsMaxConnectionsPerIp: z.number().int().positive().nullable().default(null).meta({
|
|
370
393
|
runtimeMutable: true,
|
|
371
394
|
description: "Backstop OPT-IN : plafond de connexions LONGUES CONCURRENTES par IP cliente — sockets WebSocket ET flux SSE, un seul budget pour les deux. `null` (DÉFAUT) = désactivé. Distinct de `rateLimit` (qui borne le DÉBIT d'ouverture par fenêtre) : ici on borne le NOMBRE de connexions simultanément ouvertes par IP. Au-delà, l'upgrade WebSocket est fermé (RFC 6455 close 1013) et un flux SSE répond 429. ⚠️ PORTÉE PAR PROCESS (1 pod) : un vrai plafond GLOBAL/IP se fait à l'INGRESS/LB — nginx `limit_conn`, HAProxy `sc_conn_cur`, annotation k8s `nginx.ingress.kubernetes.io/limit-connections` — qui voit TOUT le trafic, rejette AVANT que l'app paie le fd + le handshake TLS, et couvre tous les pods. En cloud-native, laisser `null` et déléguer à l'edge. N'activer (ex. 20) que sur bare-metal/VPS SANS ingress, comme défense en profondeur. IP résolue forwarded-aware (RFC 7239 + trustProxy)."
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { grpcStatusName, grpcWireOf } from "../src/grpc/refusal.js";
|
|
2
|
+
import { Code, ConnectError, createContextValues } from "@connectrpc/connect";
|
|
3
|
+
// nodefony/grpc/GrpcCall.ts
|
|
4
|
+
/** Code 0 `OK` : Connect n'en a pas dans son énumération (`Code` part de 1). */
|
|
5
|
+
const OK = 0;
|
|
6
|
+
/**
|
|
7
|
+
* Fiche d'un appel gRPC : ce que Nodefony attache à l'appel, et le
|
|
8
|
+
* `ContextValues` que Connect remet au gestionnaire (`ctx.values`).
|
|
9
|
+
*
|
|
10
|
+
* Connect alloue à chaque appel un `ContextValues` fait d'un objet littéral et
|
|
11
|
+
* de trois fermetures (`context-values.js#createContextValues`), dont la forme
|
|
12
|
+
* change à chaque `set`. La fiche le REMPLACE (option `contextValues` de
|
|
13
|
+
* l'adaptateur) : une classe à champs déclarés, méthodes au prototype, une
|
|
14
|
+
* allocation par appel au lieu de quatre.
|
|
15
|
+
*
|
|
16
|
+
* Les clés posées par un tiers (intercepteur, porte) sont confiées à un
|
|
17
|
+
* `createContextValues()` créé au premier `set` : aucune allocation tant que
|
|
18
|
+
* personne n'en pose, et aucune conversion de type — la valeur rendue est celle
|
|
19
|
+
* que le magasin de Connect a typée.
|
|
20
|
+
*
|
|
21
|
+
* C'est aussi l'appel RPC du contexte (`HttpContext.rpc`, ADR-0026 D10) : à
|
|
22
|
+
* l'échéance du délai de réponse, le noyau le termine par {@link expire} ; au
|
|
23
|
+
* `close` de la réponse, il en fixe le code par {@link settle}, que lisent la
|
|
24
|
+
* ligne d'accès, l'audit et le canal de traçage.
|
|
25
|
+
*
|
|
26
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
27
|
+
*/
|
|
28
|
+
var GrpcCall = class {
|
|
29
|
+
/**
|
|
30
|
+
* L'objet qui implémente le service pour CET appel : le contrôleur, une
|
|
31
|
+
* instance par appel sous `@Scope("request")`. `null` quand l'adaptateur est
|
|
32
|
+
* appelé hors de {@link GrpcEndpoint.serve} (jamais en service normal).
|
|
33
|
+
*/
|
|
34
|
+
target;
|
|
35
|
+
/**
|
|
36
|
+
* Le contexte de la requête qui porte l'appel : l'identité y est lue
|
|
37
|
+
* (`authenticatedUser()`) sur son magasin, jamais par l'ALS, pour choisir
|
|
38
|
+
* entre 16 et 7 (ADR-0026 D5, D7). `null` hors de {@link GrpcEndpoint.serve}.
|
|
39
|
+
*/
|
|
40
|
+
context;
|
|
41
|
+
/** Magasin des clés de tiers, créé au premier `set`. */
|
|
42
|
+
#values = null;
|
|
43
|
+
/** Contexte d'appel du gestionnaire en cours (unaire, flux). */
|
|
44
|
+
#ctx = null;
|
|
45
|
+
/** Rejet de la promesse rendue à l'adaptateur (réponse unique). */
|
|
46
|
+
#reject = null;
|
|
47
|
+
/**
|
|
48
|
+
* Pourquoi le serveur a interrompu l'appel — délai échu (4), identité
|
|
49
|
+
* révoquée (16), arrêt du serveur (14) —, `null` tant qu'il ne l'a pas fait. Posé une fois : la
|
|
50
|
+
* première cause gagne.
|
|
51
|
+
*/
|
|
52
|
+
#reason = null;
|
|
53
|
+
/**
|
|
54
|
+
* Le contexte d'appel que Connect crée AVANT de lire le corps (remis par
|
|
55
|
+
* {@link open}), gardé jusqu'au `close` : son signal porte la cause de la
|
|
56
|
+
* fin, refus de Connect avant le gestionnaire compris. `null` si l'appel n'a
|
|
57
|
+
* jamais commencé (refusé par le transport).
|
|
58
|
+
*/
|
|
59
|
+
#call = null;
|
|
60
|
+
/** Code de l'appel, fixé une fois par {@link settle}. */
|
|
61
|
+
#code = null;
|
|
62
|
+
/** `paquet.Service/Méthode`, tiré du chemin à la première lecture. */
|
|
63
|
+
#method = null;
|
|
64
|
+
constructor(target, context = null) {
|
|
65
|
+
this.target = target;
|
|
66
|
+
this.context = context;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Le protocole parlé sur le fil : celui du contexte d'appel (`grpc`,
|
|
70
|
+
* `grpc-web`, `connect`) ; à défaut, celui des en-têtes de la requête.
|
|
71
|
+
*/
|
|
72
|
+
get system() {
|
|
73
|
+
const call = this.#call;
|
|
74
|
+
if (call !== null) return call.protocolName;
|
|
75
|
+
const request = this.context?.request.request;
|
|
76
|
+
const wire = request === void 0 ? 0 : grpcWireOf(request.headers);
|
|
77
|
+
if (wire === 1) return "grpc";
|
|
78
|
+
if (wire === 2) return "grpc-web";
|
|
79
|
+
return "connect";
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* `paquet.Service/Méthode` : le chemin de la route, que l'adaptateur a
|
|
83
|
+
* reconnu à l'octet près. Lu à la première demande, puis retenu.
|
|
84
|
+
*/
|
|
85
|
+
get method() {
|
|
86
|
+
if (this.#method !== null) return this.#method;
|
|
87
|
+
const call = this.#call;
|
|
88
|
+
const path = this.context?.request.url.pathname;
|
|
89
|
+
this.#method = path !== void 0 ? path.slice(1) : call !== null ? `${call.service.typeName}/${call.method.name}` : "";
|
|
90
|
+
return this.#method;
|
|
91
|
+
}
|
|
92
|
+
/** Le code de l'appel (0 à 16), `null` avant {@link settle}. */
|
|
93
|
+
get code() {
|
|
94
|
+
return this.#code;
|
|
95
|
+
}
|
|
96
|
+
/** Le nom du code (`OK`, `UNAVAILABLE`…), `null` avec lui. */
|
|
97
|
+
get codeName() {
|
|
98
|
+
return this.#code === null ? null : grpcStatusName(this.#code);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Garde le contexte d'appel que Connect vient de créer, avant le corps
|
|
102
|
+
* (option `requestGate` de l'adaptateur, en simple observateur).
|
|
103
|
+
*
|
|
104
|
+
* @param ctx - le contexte d'appel.
|
|
105
|
+
*
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
open(ctx) {
|
|
109
|
+
this.#call = ctx;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Fixe le code de l'appel au `close` de la réponse (ADR-0026 D10 point 2) :
|
|
113
|
+
*
|
|
114
|
+
* - motif `ConnectError` → son code ; sa cause, si c'est une `Error`, part
|
|
115
|
+
* à la ligne d'accès (exception traduite par D7). Réponse NON finie : 1,
|
|
116
|
+
* sauf une fin posée par le serveur (échéance 4, interruption de la fiche)
|
|
117
|
+
* — le client est parti, quel que soit le code de son `RST_STREAM` ;
|
|
118
|
+
* - `AbortError` ou aucun motif → 0 si la réponse est finie, sinon 1 (le
|
|
119
|
+
* client est parti — le 499 du noyau, dit en gRPC) ;
|
|
120
|
+
* - autre valeur → 13, et elle part à la ligne d'accès.
|
|
121
|
+
*
|
|
122
|
+
* Une interruption du serveur (délai échu, identité révoquée) abaisse le
|
|
123
|
+
* signal avec sa `ConnectError` : elle suit la première règle. Sans contexte
|
|
124
|
+
* d'appel, le code reste `null`.
|
|
125
|
+
*
|
|
126
|
+
* @param ended - la réponse a-t-elle été terminée par le serveur ?
|
|
127
|
+
* @returns la cause à porter par la ligne d'accès, `null` sinon.
|
|
128
|
+
*/
|
|
129
|
+
settle(ended) {
|
|
130
|
+
const call = this.#call;
|
|
131
|
+
if (this.#code !== null || call === null) return null;
|
|
132
|
+
const reason = call.signal.reason;
|
|
133
|
+
if (reason instanceof ConnectError) {
|
|
134
|
+
if (!ended && reason !== this.#reason && reason.code !== Code.DeadlineExceeded) {
|
|
135
|
+
this.#code = Code.Canceled;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
this.#code = reason.code;
|
|
139
|
+
return reason.cause instanceof Error ? reason.cause : null;
|
|
140
|
+
}
|
|
141
|
+
if (endsWithoutError(reason)) {
|
|
142
|
+
this.#code = ended ? OK : Code.Canceled;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
this.#code = Code.Internal;
|
|
146
|
+
return reason instanceof Error ? reason : new Error("gRPC : le gestionnaire a levé une valeur qui n'est pas une Error", { cause: reason });
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Lit une valeur posée par un tiers, ou la valeur par défaut de sa clé.
|
|
150
|
+
*
|
|
151
|
+
* @param key - la clé (`createContextKey`).
|
|
152
|
+
* @returns la valeur posée, sinon `key.defaultValue`.
|
|
153
|
+
*/
|
|
154
|
+
get(key) {
|
|
155
|
+
return this.#values === null ? key.defaultValue : this.#values.get(key);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Pose une valeur pour la durée de l'appel.
|
|
159
|
+
*
|
|
160
|
+
* @param key - la clé (`createContextKey`).
|
|
161
|
+
* @param value - la valeur.
|
|
162
|
+
* @returns la fiche, pour chaîner.
|
|
163
|
+
*/
|
|
164
|
+
set(key, value) {
|
|
165
|
+
(this.#values ??= createContextValues()).set(key, value);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Retire une valeur ; sa clé rendra de nouveau sa valeur par défaut.
|
|
170
|
+
*
|
|
171
|
+
* @param key - la clé (`createContextKey`).
|
|
172
|
+
* @returns la fiche, pour chaîner.
|
|
173
|
+
*/
|
|
174
|
+
delete(key) {
|
|
175
|
+
this.#values?.delete(key);
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Le délai de réponse du serveur est échu : le signal de l'appel s'abaisse
|
|
180
|
+
* (`ctx.abort`) et l'appel se termine en 4 `DEADLINE_EXCEEDED`, même si le
|
|
181
|
+
* gestionnaire ne lit pas son signal. Échu avant que le gestionnaire ne
|
|
182
|
+
* démarre : il est terminé dès son démarrage. Sans effet après la fin.
|
|
183
|
+
*/
|
|
184
|
+
expire() {
|
|
185
|
+
if (this.#reason !== null) return;
|
|
186
|
+
this.#interrupt(new ConnectError("délai de réponse du serveur dépassé", Code.DeadlineExceeded));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* L'identité qui a ouvert l'appel n'est plus valable : le signal de l'appel
|
|
190
|
+
* s'abaisse et l'appel se termine en 16 `UNAUTHENTICATED`. Réponse unique :
|
|
191
|
+
* sa promesse est rejetée. Flux : il s'arrête au message suivant, même si le
|
|
192
|
+
* gestionnaire ne lit pas son signal — au plus le message déjà en vol part.
|
|
193
|
+
* Sans effet après la fin, ou après une autre interruption.
|
|
194
|
+
*/
|
|
195
|
+
revoke() {
|
|
196
|
+
if (this.#reason !== null) return;
|
|
197
|
+
this.#interrupt(new ConnectError("identité révoquée", Code.Unauthenticated));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Le serveur s'arrête : le signal de l'appel s'abaisse et l'appel se termine
|
|
201
|
+
* en 14 `UNAVAILABLE` — le client le rejoue ailleurs (ADR-0026 D8). Flux : il
|
|
202
|
+
* s'arrête au message suivant, même si le gestionnaire ne lit pas son
|
|
203
|
+
* signal. Un gestionnaire bloqué qui n'écoute rien tombe avec sa connexion, à
|
|
204
|
+
* `shutdownTimeout`. Sans effet après la fin, ou après une autre interruption.
|
|
205
|
+
*/
|
|
206
|
+
shutdown() {
|
|
207
|
+
if (this.#reason !== null) return;
|
|
208
|
+
this.#interrupt(new ConnectError("le serveur s'arrête", Code.Unavailable));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* L'interruption posée par le serveur, `null` tant qu'il n'y en a pas — lue
|
|
212
|
+
* par l'enrobage d'un flux avant chaque message.
|
|
213
|
+
*
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
get interruption() {
|
|
217
|
+
return this.#reason;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Suit un gestionnaire à réponse unique (unaire, flux client) jusqu'à sa
|
|
221
|
+
* fin : une interruption pourra rejeter sa promesse.
|
|
222
|
+
*
|
|
223
|
+
* @param ctx - le contexte d'appel remis au gestionnaire.
|
|
224
|
+
* @param reject - le rejet de la promesse rendue à l'adaptateur.
|
|
225
|
+
*
|
|
226
|
+
* @internal
|
|
227
|
+
*/
|
|
228
|
+
attach(ctx, reject) {
|
|
229
|
+
this.#ctx = ctx;
|
|
230
|
+
const reason = this.#reason;
|
|
231
|
+
if (reason !== null) {
|
|
232
|
+
this.#end(reason, reject);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.#reject = reject;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Suit un gestionnaire en flux (serveur, bidirectionnel) : une interruption
|
|
239
|
+
* abaissera son signal. Son itérateur s'arrête par {@link interruption}.
|
|
240
|
+
*
|
|
241
|
+
* @param ctx - le contexte d'appel remis au gestionnaire.
|
|
242
|
+
*
|
|
243
|
+
* @internal
|
|
244
|
+
*/
|
|
245
|
+
follow(ctx) {
|
|
246
|
+
this.#ctx = ctx;
|
|
247
|
+
const reason = this.#reason;
|
|
248
|
+
if (reason !== null && hasAbort(ctx)) ctx.abort(reason);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Le gestionnaire a répondu ou levé : plus rien à terminer.
|
|
252
|
+
*
|
|
253
|
+
* @internal
|
|
254
|
+
*/
|
|
255
|
+
detach() {
|
|
256
|
+
this.#ctx = null;
|
|
257
|
+
this.#reject = null;
|
|
258
|
+
}
|
|
259
|
+
#interrupt(reason) {
|
|
260
|
+
this.#reason = reason;
|
|
261
|
+
const reject = this.#reject;
|
|
262
|
+
if (reject !== null) {
|
|
263
|
+
this.#end(reason, reject);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const ctx = this.#ctx;
|
|
267
|
+
if (ctx !== null && hasAbort(ctx)) ctx.abort(reason);
|
|
268
|
+
}
|
|
269
|
+
#end(reason, reject) {
|
|
270
|
+
const ctx = this.#ctx;
|
|
271
|
+
this.detach();
|
|
272
|
+
if (ctx !== null && hasAbort(ctx)) ctx.abort(reason);
|
|
273
|
+
reject(reason);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Garde de type : le contexte d'appel porte-t-il `abort` ? Connect le pose sur
|
|
278
|
+
* tout contexte qu'il crée ; le garde le CONSTATE au lieu de le supposer, pour
|
|
279
|
+
* qu'une version de Connect qui le retirerait se voie à l'exécution (et au
|
|
280
|
+
* test qui le garde) plutôt que par un appel qui ne s'interrompt plus.
|
|
281
|
+
*
|
|
282
|
+
* @param ctx - le contexte remis au gestionnaire.
|
|
283
|
+
* @returns `true` si `ctx.abort` est une fonction.
|
|
284
|
+
*
|
|
285
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
286
|
+
*/
|
|
287
|
+
function hasAbort(ctx) {
|
|
288
|
+
return "abort" in ctx && typeof ctx.abort === "function";
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Le motif d'une fin SANS erreur : aucun (le signal n'est pas abaissé — en
|
|
292
|
+
* HTTP/1.1, l'écouteur `close` du noyau passe avant celui de Connect), ou un
|
|
293
|
+
* `AbortError` (fin normale, ou client parti).
|
|
294
|
+
*/
|
|
295
|
+
function endsWithoutError(reason) {
|
|
296
|
+
return reason === void 0 || reason instanceof Error && reason.name === "AbortError";
|
|
297
|
+
}
|
|
298
|
+
export { GrpcCall, hasAbort };
|