@nodefony/realtime 10.0.0-alpha.1

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.
Files changed (81) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +207 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +218 -0
  6. package/dist/nodefony/config/config.js +63 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
  9. package/dist/nodefony/interfaces/IBackplane.js +1 -0
  10. package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
  11. package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
  12. package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
  13. package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
  14. package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
  15. package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
  16. package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
  17. package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
  18. package/dist/nodefony/interfaces/index.js +1 -0
  19. package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
  20. package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
  21. package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
  22. package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
  23. package/dist/nodefony/src/backplane/envelope.js +109 -0
  24. package/dist/nodefony/src/backplane/originId.js +35 -0
  25. package/dist/nodefony/src/backplane/publishQueue.js +96 -0
  26. package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
  27. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
  28. package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
  29. package/dist/nodefony/src/server/RealtimeController.js +601 -0
  30. package/dist/nodefony/src/server/RealtimeHub.js +926 -0
  31. package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
  32. package/dist/nodefony/src/server/deniedDetail.js +29 -0
  33. package/dist/nodefony/src/server/syslogUplink.js +105 -0
  34. package/dist/nodefony/src/server/welcomeEnv.js +25 -0
  35. package/dist/nodefony/src/service/RealtimeService.js +240 -0
  36. package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
  37. package/dist/nodefony/testing/index.js +163 -0
  38. package/dist/types/index.d.ts +99 -0
  39. package/dist/types/nodefony/config/config.d.ts +43 -0
  40. package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
  41. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
  42. package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
  43. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
  44. package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
  45. package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
  46. package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
  47. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
  48. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
  49. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
  50. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +18 -0
  52. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
  53. package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
  54. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
  55. package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
  56. package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
  57. package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
  58. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
  59. package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
  60. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
  61. package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
  62. package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
  63. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
  64. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
  65. package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
  66. package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
  67. package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
  68. package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
  69. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
  70. package/dist/types/nodefony/testing/index.d.ts +169 -0
  71. package/docs/actions.md +573 -0
  72. package/docs/architecture.md +889 -0
  73. package/docs/configuration.md +803 -0
  74. package/docs/cookbook-chat.md +1619 -0
  75. package/docs/couverture-bancs.md +139 -0
  76. package/docs/index.md +372 -0
  77. package/docs/observabilite.md +592 -0
  78. package/docs/protocole.md +518 -0
  79. package/docs/securite.md +845 -0
  80. package/docs/vocabulaire.md +845 -0
  81. package/package.json +83 -0
@@ -0,0 +1,76 @@
1
+ import type { IBackplane, BackplaneHandler, IBackplaneMessage, IBackplaneInfo } from "../../interfaces/IBackplane.js";
2
+ import { CLUSTER_RT_KIND } from "nodefony";
3
+ /**
4
+ * Enveloppe IPC d'une publication realtime = {@link IBackplaneMessage} + discriminant.
5
+ * Sérialisée une fois par `process.send`/`worker.send` (`structuredClone` IPC) → le
6
+ * `payload` doit rester structurellement clonable (cf contrat IBackplane).
7
+ */
8
+ export interface ClusterBackplaneEnvelope extends IBackplaneMessage {
9
+ readonly kind: typeof CLUSTER_RT_KIND;
10
+ }
11
+ /**
12
+ * Transport IPC du backplane cluster — abstrait `process.send` / la réception des
13
+ * messages relayés par le master, derrière un seam **injectable** (même pattern que
14
+ * `IClusterRuntime` côté ClusterManager). Permet de prouver le routage + l'anti-echo
15
+ * **sans forker** de process réel.
16
+ */
17
+ export interface IClusterBackplaneTransport {
18
+ /**
19
+ * Émet une enveloppe vers le master (worker → master). No-op silencieux si le
20
+ * canal IPC est absent (process hors cluster) — le backplane reste sûr.
21
+ */
22
+ send(env: ClusterBackplaneEnvelope): void;
23
+ /**
24
+ * Enregistre le récepteur des messages IPC venus du master (master → worker).
25
+ * Reçoit `unknown` (canal partagé) : le tri du `kind` est fait par le backplane.
26
+ * Appelé une seule fois, au `start()`.
27
+ */
28
+ onReceive(cb: (msg: unknown) => void): void;
29
+ }
30
+ /**
31
+ * Transport de production : `process.send` (émission) + `process.on("message")`
32
+ * (réception). Standalone — utilisable tel quel. En vrai câblage cluster (Phase 3c),
33
+ * un transport branché sur l'event kernel `onMessage` (déjà alimenté par
34
+ * `Kernel.initCluster`) est préféré pour éviter un 2ᵉ listener + le log par message.
35
+ */
36
+ export declare const processIpcTransport: IClusterBackplaneTransport;
37
+ /**
38
+ * Backplane **IPC cross-process** (Phase 3) — implémentation worker-side du port
39
+ * {@link IBackplane} dans un cluster Node natif, où les workers ne se parlent qu'à
40
+ * travers le **master-gateway** (un worker ne peut envoyer qu'au master via IPC).
41
+ *
42
+ * Flux d'une publication cross-process :
43
+ * - {@link publish} → emballe `{kind,channel,payload,originId}` → `transport.send`
44
+ * (worker → master) ; le master rebroadcast aux **autres** workers (cf `ClusterRelay`,
45
+ * Phase 3b) ; le fan-out **local** a déjà été fait par le hub avant cet appel ;
46
+ * - réception (master → worker) → {@link onMessage} handler → le hub réinjecte en
47
+ * fan-out **local uniquement** (`publishLocal`), jamais re-publié (anti-boucle).
48
+ *
49
+ * Anti-echo (2ᵉ barrière du contrat) : à la réception, on **filtre son propre
50
+ * `originId`** — même si le master renvoyait à la source, le worker l'ignore.
51
+ *
52
+ * Perf : hors mono-process (où le hub garde `#backplane === null` → ce code ne tourne
53
+ * jamais). En cluster, `publish` alloue 1 enveloppe + 1 sérialisation IPC (coût dominé
54
+ * par l'IPC, pas par l'alloc) — sur le chemin realtime broadcast (tickers/fan-out),
55
+ * PAS sur le hot path du request HTTP. SERVEUR uniquement (`process`).
56
+ */
57
+ export declare class ClusterBackplane implements IBackplane {
58
+ #private;
59
+ /** Nom du driver — source unique du littéral (registre + config). */
60
+ static readonly driver = "cluster";
61
+ readonly originId: string;
62
+ constructor(transport?: IClusterBackplaneTransport, originId?: string);
63
+ /** Branche la réception IPC. Idempotent. */
64
+ start(): void;
65
+ /**
66
+ * Propage une publication locale aux autres pairs via le master. NE refait PAS le
67
+ * fan-out local (déjà fait par le hub). Sérialisé par le transport IPC.
68
+ */
69
+ publish(channel: string, payload: unknown): void;
70
+ /** Un seul handler d'ingress ; un appel ultérieur remplace le précédent. */
71
+ onMessage(handler: BackplaneHandler): void;
72
+ /** Détache le handler. Le listener du transport prod (`process`) survit au process. */
73
+ stop(): void;
74
+ describe(): IBackplaneInfo;
75
+ }
76
+ export default ClusterBackplane;
@@ -0,0 +1,29 @@
1
+ import type { IBackplane, BackplaneHandler, IBackplaneInfo } from "../../interfaces/IBackplane.js";
2
+ /**
3
+ * Backplane **mono-process no-op** — implémentation de RÉFÉRENCE du port
4
+ * {@link IBackplane} quand il n'existe **aucun pair** (1 process = 1 pod).
5
+ *
6
+ * `publish` ne sort rien (pas de pair à qui propager), `onMessage` ne fire jamais
7
+ * (rien n'arrive de l'extérieur) → 0 fan-out cross-process, le hub se comporte
8
+ * exactement comme en local pur.
9
+ *
10
+ * Rôle : (1) matérialiser le contrat pour les tests (cible de vérification du câblage
11
+ * du hub) ; (2) câblage explicite « backplane présent mais inactif ». NB perf : en
12
+ * mono-process le {@link RealtimeHub} garde `#backplane === null` (0 overhead réel,
13
+ * style lazy du fichier) — ce Loopback n'est donc PAS sur le hot path par défaut, il
14
+ * sert à prouver que brancher un backplane ne change rien tant qu'il n'y a pas de pair.
15
+ *
16
+ * SERVEUR uniquement (`process.pid`) — pas isomorphe.
17
+ */
18
+ export declare class LoopbackBackplane implements IBackplane {
19
+ /** Nom du driver — source unique du littéral (registre + config). */
20
+ static readonly driver = "loopback";
21
+ readonly originId: string;
22
+ constructor(originId?: string);
23
+ start(): void;
24
+ publish(_channel: string, _payload: unknown): void;
25
+ onMessage(_handler: BackplaneHandler): void;
26
+ stop(): void;
27
+ describe(): IBackplaneInfo;
28
+ }
29
+ export default LoopbackBackplane;
@@ -0,0 +1,164 @@
1
+ import type { IBackplane, BackplaneHandler, IBackplaneInfo } from "../../interfaces/IBackplane.js";
2
+ import { type BackplaneNotice } from "./publishQueue.js";
3
+ /**
4
+ * BASE du canal Redis **dédié** transportant les enveloppes realtime entre pods.
5
+ * Un seul canal pub/sub par app (les canaux logiques voyagent DANS l'enveloppe,
6
+ * comme le `channel` IPC du {@link ClusterBackplane}) → 1 `SUBSCRIBE` au boot, pas
7
+ * de (dés)abonnement Redis dynamique par canal applicatif.
8
+ *
9
+ * ⚠️ Le `database` Redis ne cloisonne PAS le pub/sub (global au serveur) → sur un
10
+ * Redis mutualisé, deux apps sur le même canal se parlent (cross-talk). Le canal
11
+ * effectif est donc suffixé par un namespace via {@link resolveRedisChannel}
12
+ * (config `backplane.namespace`, sinon dérivé de `kernel.projectName` au wiring).
13
+ */
14
+ export declare const REDIS_RT_CHANNEL = "nodefony:realtime";
15
+ /**
16
+ * Construit le canal pub/sub effectif : `nodefony:realtime:<namespace>`, ou la
17
+ * base seule si aucun namespace n'est résolu (compat mono-app). Deux déploiements
18
+ * de la MÊME app sur un Redis partagé (staging + prod) ont le même nom dérivé →
19
+ * y poser un `backplane.namespace` EXPLICITE distinct.
20
+ *
21
+ * @param namespace - cloison logique (config `backplane.namespace` ou nom d'app).
22
+ * @returns le nom du canal Redis à `SUBSCRIBE`/`PUBLISH`.
23
+ */
24
+ export declare function resolveRedisChannel(namespace?: string): string;
25
+ /**
26
+ * Transport pub/sub du backplane Redis — abstrait `PUBLISH` / `SUBSCRIBE` derrière
27
+ * un seam **injectable** (même pattern que `IClusterBackplaneTransport`). Permet de
28
+ * prouver le routage + l'anti-echo **sans Redis** (bus en mémoire dans les tests),
29
+ * et de ne PAS coupler `@nodefony/realtime` à la lib `redis` ni au module
30
+ * `@nodefony/redis` (cf {@link createRedisServiceTransport}, le seul point couplé).
31
+ */
32
+ export interface IRedisBackplaneTransport {
33
+ /**
34
+ * Publie un message sérialisé sur le canal Redis.
35
+ *
36
+ * Rendre la promesse du client (plutôt que `void`) permet au backplane de savoir
37
+ * QUAND la publication est acquittée, donc de **borner** ce qui est en vol : sans
38
+ * ce signal, la file interne du client réseau grossit sans limite sous rafale (cf
39
+ * {@link BackplanePublishQueue}). Un transport synchrone (bus mémoire de test) rend
40
+ * `void` — il n'a pas de file, la borne reste alors inerte.
41
+ */
42
+ publish(channel: string, message: string): void | Promise<unknown>;
43
+ /**
44
+ * Abonne au canal Redis. Le listener reçoit le message brut (string) ; le tri
45
+ * (parse + anti-echo) est fait par le backplane. Async (Redis) ou sync (fake).
46
+ */
47
+ subscribe(channel: string, onMessage: (message: string) => void): void | Promise<void>;
48
+ /** Désabonne du canal Redis et libère le listener associé. Idempotent. */
49
+ unsubscribe(channel: string): void | Promise<void>;
50
+ }
51
+ /**
52
+ * Client Redis **publisher** — surface structurelle minimale (compatible
53
+ * `RedisClientType` v5). Typage structurel volontaire : évite d'ajouter `redis`
54
+ * en dépendance de `@nodefony/realtime`.
55
+ */
56
+ export interface IRedisPublisher {
57
+ publish(channel: string, message: string): unknown;
58
+ }
59
+ /**
60
+ * Client Redis **subscriber** dédié — surface structurelle minimale (compatible
61
+ * `RedisClientType` v5). Un client abonné ne peut plus émettre de commandes
62
+ * normales (protocole Redis) → publisher et subscriber sont 2 connexions distinctes
63
+ * (cf `@nodefony/redis` : connexions `publish` / `subscribe`).
64
+ */
65
+ export interface IRedisSubscriber {
66
+ subscribe(channel: string, listener: (message: string, channel: string) => unknown): unknown;
67
+ unsubscribe(channel: string, listener?: (message: string, channel: string) => unknown): unknown;
68
+ }
69
+ /**
70
+ * Adapte deux clients Redis (publisher + subscriber dédié) au seam
71
+ * {@link IRedisBackplaneTransport}. SEUL point de contact avec la lib `redis` —
72
+ * et même ici le couplage est **structurel** (aucun import). Le consommateur
73
+ * branche typiquement les connexions du module `@nodefony/redis` :
74
+ *
75
+ * ```ts
76
+ * const redis = kernel.get("redis"); // RedisService
77
+ * const bp = new RedisBackplane(
78
+ * createRedisServiceTransport(redis.getClient("publish")!, redis.getClient("subscribe")!),
79
+ * );
80
+ * defineRealtimeConfig({ backplane: { driver: "redis" } }, { backplane: bp });
81
+ * ```
82
+ *
83
+ * Le listener Redis est conservé pour un `unsubscribe(channel, listener)` ciblé
84
+ * (n'arrache pas d'éventuels autres abonnés du même client).
85
+ */
86
+ export declare function createRedisServiceTransport(publisher: IRedisPublisher, subscriber: IRedisSubscriber): IRedisBackplaneTransport;
87
+ /**
88
+ * Réglages optionnels du driver — regroupés en objet pour ne pas allonger une
89
+ * liste de paramètres positionnels déjà à quatre entrées.
90
+ */
91
+ export interface IRedisBackplaneOptions {
92
+ /**
93
+ * Seuil d'octets publiés en attente d'acquittement au-delà duquel les
94
+ * publications sont jetées ; `0` = illimité (opt-out explicite). Défaut :
95
+ * {@link DEFAULT_MAX_QUEUE_BYTES}.
96
+ */
97
+ maxQueueBytes?: number;
98
+ /**
99
+ * Annonce des transitions de la file (saturation / retour à la normale) — le
100
+ * wiring y branche le syslog du module. Omis = silencieux : acceptable en test,
101
+ * jamais en production (une dégradation doit être annoncée).
102
+ */
103
+ onNotice?: BackplaneNotice;
104
+ }
105
+ /**
106
+ * Backplane **Redis pub/sub** (P13.5) — implémentation du port {@link IBackplane}
107
+ * pour le fan-out realtime **cross-pod** (multi-host), là où le {@link ClusterBackplane}
108
+ * ne couvrait que les workers d'un même pod (IPC). **Drop-in** : le hub ne change pas,
109
+ * seul le driver branché change.
110
+ *
111
+ * Flux d'une publication cross-pod :
112
+ * - {@link publish} → emballe `{channel,payload,originId}` → `PUBLISH nodefony:realtime`
113
+ * (le fan-out **local** a déjà été fait par le hub avant cet appel) ;
114
+ * - réception (`SUBSCRIBE`) → {@link onMessage} handler → le hub réinjecte en
115
+ * fan-out **local uniquement** (`publishLocal`), jamais re-publié (anti-boucle).
116
+ *
117
+ * Anti-echo (2ᵉ barrière du contrat) : Redis renvoie au pod émetteur ce qu'il
118
+ * publie (publisher + subscriber sont 2 connexions du même pod) → on **filtre son
119
+ * propre `originId`** à la réception, sinon double fan-out local.
120
+ *
121
+ * Authenticité (F83) : Redis pub/sub n'authentifie PAS l'émetteur d'un message —
122
+ * quiconque écrit dans ce Redis (autre app d'un Redis mutualisé, credential fuité,
123
+ * SSRF vers le port) publierait sur les canaux de tous les pods. Avec un `secret`
124
+ * partagé, l'enveloppe est **scellée** (HMAC, cf `envelope.ts`) et l'ingress devient
125
+ * fail-closed strict : non scellé ou mal scellé = ignoré, sans downgrade possible.
126
+ * Sans secret le transport reste ouvert (compat) — le wiring alerte alors au boot.
127
+ *
128
+ * Livraison : **best-effort / at-most-once** — pub/sub Redis ne persiste ni ne
129
+ * rejoue ; un pod déconnecté rate les messages émis pendant sa coupure (le client
130
+ * realtime re-synchronise). Ne pas sur-concevoir une fiabilité que le backing
131
+ * n'offre pas (cf contrat {@link IBackplane}).
132
+ *
133
+ * Perf : hors mono-process (où le hub garde `#backplane === null`). `publish`
134
+ * alloue 1 enveloppe + 1 `JSON.stringify` — sur le chemin broadcast realtime
135
+ * (tickers/fan-out), PAS sur le hot path HTTP. SERVEUR uniquement (`process.pid`).
136
+ */
137
+ export declare class RedisBackplane implements IBackplane {
138
+ #private;
139
+ /** Nom du driver — source unique du littéral (registre + config). */
140
+ static readonly driver = "redis";
141
+ readonly originId: string;
142
+ constructor(transport: IRedisBackplaneTransport, originId?: string, redisChannel?: string, secret?: string | null, options?: IRedisBackplaneOptions);
143
+ /**
144
+ * Abonne au canal Redis dédié. Idempotent (le flag est posé avant l'`await` →
145
+ * un 2ᵉ appel concurrent ne ré-abonne pas).
146
+ */
147
+ start(): Promise<void>;
148
+ /**
149
+ * Propage une publication locale aux autres pods via Redis. NE refait PAS le
150
+ * fan-out local (déjà fait par le hub). Sérialisé une fois ici.
151
+ *
152
+ * L'envoi passe par la **file bornée** : si le bus n'acquitte plus, les
153
+ * publications suivantes sont jetées et comptées plutôt que d'empiler des
154
+ * mégaoctets dans le client Redis (sémantique at-most-once déjà assumée par le
155
+ * port ; le client realtime re-synchronise).
156
+ */
157
+ publish(channel: string, payload: unknown): void;
158
+ /** Un seul handler d'ingress ; un appel ultérieur remplace le précédent. */
159
+ onMessage(handler: BackplaneHandler): void;
160
+ /** Désabonne du canal Redis et détache le handler. Idempotent. */
161
+ stop(): Promise<void>;
162
+ describe(): IBackplaneInfo;
163
+ }
164
+ export default RedisBackplane;
@@ -0,0 +1,51 @@
1
+ import type { Module } from "nodefony";
2
+ import type { IBackplane } from "../../interfaces/IBackplane.js";
3
+ import type { IRealtimeConfig } from "../../config/defineModuleConfig.js";
4
+ /**
5
+ * Registre de **drivers backplane** — résout `config.backplane.driver` (une
6
+ * chaîne) vers une fabrique d'`IBackplane`, SANS que le code de wiring connaisse
7
+ * le moindre nom de driver en dur.
8
+ *
9
+ * Pourquoi : `IBackplane` est pluggable par contrat. Sélectionner le driver avec
10
+ * une chaîne de `if (driver === "redis") … else if (driver === "kafka") …` dans
11
+ * le Module trahit cette promesse (couplage aux noms, fermé à l'extension). Le
12
+ * registre rend la sélection **ouverte** : les drivers natifs s'enregistrent au
13
+ * chargement du module, et un utilisateur peut enregistrer le sien
14
+ * (`registerBackplaneDriver("nats", …)`) puis poser `driver: "nats"` en config —
15
+ * aucun changement dans le cœur. Single source of truth de la résolution driver.
16
+ */
17
+ /**
18
+ * Contexte passé à une fabrique de backplane : tout ce dont un driver peut avoir
19
+ * besoin pour se construire (accès kernel/container/log via le Module, identité
20
+ * du pair, rôle dans la topologie, config validée). Une fabrique ne démarre PAS
21
+ * le backplane (`start()` est appelé par le wiring) — elle le **construit** ou
22
+ * renvoie `null` (driver inactif dans ce contexte, ex. loopback / cluster hors
23
+ * worker → le hub reste local).
24
+ */
25
+ export interface IBackplaneFactoryContext {
26
+ /** Module realtime — accès `container`/`kernel`/`log`. */
27
+ readonly module: Module;
28
+ /** Identité stable de ce pair (process/pod). Ex. `String(process.pid)`. */
29
+ readonly originId: string;
30
+ /** Rôle du process dans la topologie de lancement. */
31
+ readonly role: "MASTER" | "WORKER" | "MONO";
32
+ /** Config realtime validée + gelée. */
33
+ readonly config: IRealtimeConfig;
34
+ }
35
+ /**
36
+ * Fabrique d'un backplane pour un driver donné. Retourne l'instance à brancher,
37
+ * ou `null` si le driver est inactif dans ce contexte (mono-process, mauvais
38
+ * rôle, infra absente — le wiring laisse alors le hub local). Peut être async
39
+ * (ex. lecture d'un service dont l'init est asynchrone).
40
+ */
41
+ export type BackplaneFactory = (ctx: IBackplaneFactoryContext) => IBackplane | null | Promise<IBackplane | null>;
42
+ /**
43
+ * Enregistre (ou remplace) la fabrique d'un driver backplane. Appelé par les
44
+ * drivers natifs au chargement du module, et par l'utilisateur pour ses drivers
45
+ * custom (NATS, Pulsar, RabbitMQ…).
46
+ */
47
+ export declare function registerBackplaneDriver(name: string, factory: BackplaneFactory): void;
48
+ /** Fabrique d'un driver par nom, ou `undefined` si inconnu. */
49
+ export declare function getBackplaneDriver(name: string): BackplaneFactory | undefined;
50
+ /** Noms des drivers enregistrés (introspection / Studio / tests). */
51
+ export declare function listBackplaneDrivers(): string[];
@@ -0,0 +1,27 @@
1
+ import type { IBackplaneMessage } from "../../interfaces/IBackplane.js";
2
+ /**
3
+ * Sérialise un message backplane, scellé si un secret est fourni.
4
+ *
5
+ * L'enveloppe scellée est assemblée par concaténation des fragments déjà
6
+ * sérialisés — un `JSON.stringify` d'objet complet re-sérialiserait la charge
7
+ * une seconde fois.
8
+ *
9
+ * @param msg - message à transporter (canal, charge, origine).
10
+ * @param secret - secret partagé entre pods ; `null`/`undefined` → enveloppe nue.
11
+ * @returns la chaîne à publier sur le bus.
12
+ */
13
+ export declare function sealBackplaneEnvelope(msg: IBackplaneMessage, secret?: string | null): string;
14
+ /**
15
+ * Parse et **vérifie** une enveloppe reçue du bus partagé.
16
+ *
17
+ * Fail-closed strict quand un secret est posé : une enveloppe sans sceau ou au
18
+ * sceau invalide est rejetée — **aucun downgrade** possible en retirant le champ
19
+ * `sig`. Sans secret, l'enveloppe nue est acceptée (compat ; l'alerte de boot qui
20
+ * signale ce mode non authentifié est portée par le wiring du driver).
21
+ *
22
+ * @param raw - message brut lu sur le bus.
23
+ * @param secret - secret partagé attendu, ou `null`/`undefined` si aucun.
24
+ * @returns le message vérifié, ou `null` s'il doit être ignoré (malformé, non
25
+ * scellé alors qu'un secret est exigé, sceau invalide).
26
+ */
27
+ export declare function openBackplaneEnvelope(raw: string, secret?: string | null): IBackplaneMessage | null;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Résout l'identifiant d'origine d'un backplane — l'étiquette que l'anti-écho
3
+ * compare pour jeter les messages qui reviennent à leur émetteur.
4
+ *
5
+ * POURQUOI pas `String(process.pid)` (ex-défaut) : en conteneur, le namespace
6
+ * PID est PAR conteneur → deux pods k8s peuvent tous deux être PID 1. Avec un
7
+ * backplane cross-pod (Redis/Kafka), l'anti-écho confond alors les pods et
8
+ * jette SILENCIEUSEMENT le fan-out légitime (dette 🔴 #2 du module).
9
+ *
10
+ * Résolution (du plus explicite au filet) :
11
+ * 1. `NF_POD_NAME` (downward API k8s, opt-in opérateur) ;
12
+ * 2. `os.hostname()` — en k8s = nom du pod (unique), en docker = id court du
13
+ * conteneur (unique), en bare-metal = nom de machine ;
14
+ * 3. suffixe `:pid` TOUJOURS ajouté : distingue les workers d'un même host
15
+ * (cluster bare-metal `-w N` + driver redis = même hostname, pids ≠) ;
16
+ * 4. hostname indisponible/vide (exotique) → `randomUUID()` (l'anti-écho n'a
17
+ * pas besoin de stabilité entre restarts, seulement d'unicité à l'instant t).
18
+ *
19
+ * Évalué à la CONSTRUCTION du backplane (boot) — 0 coût par message.
20
+ */
21
+ export declare function resolveBackplaneOriginId(): string;
22
+ export default resolveBackplaneOriginId;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * **File d'envoi bornée d'un backplane** — protège la mémoire du pod quand le bus
3
+ * n'absorbe pas le débit publié. Mutualisée ici (comme le sceau d'enveloppe) pour
4
+ * qu'un driver à transport réseau — Redis, et tout driver userland : NATS, Kafka,
5
+ * RabbitMQ — hérite de la garantie sans la réécrire.
6
+ *
7
+ * Pourquoi : `IBackplane.publish` est **fire-and-forget** par contrat (il rend
8
+ * `void`). Un client réseau met alors la commande dans SA file tant que la socket
9
+ * n'est pas drainée — file interne, invisible, sans limite. Une rafale y accumule
10
+ * des mégaoctets : 583 MB observés sur le banc multi-pods, pour 152 MB au repos.
11
+ * Aucune fuite (tout redescend), mais rien ne borne le pic → OOM sous rafale plus
12
+ * longue.
13
+ *
14
+ * Doctrine — la MÊME que le transport WS (seuil, compteur de jetés, avertissement),
15
+ * parce que le problème est le même : un pair qui ne draine pas. Deux règles en
16
+ * découlent :
17
+ * - **on protège la mémoire** : au-delà du seuil, les publications suivantes sont
18
+ * jetées (sémantique at-most-once déjà assumée par le port — le client realtime
19
+ * re-synchronise) ;
20
+ * - **on ne jette jamais en silence** : compteurs exposés dans la carte d'identité
21
+ * du backplane (donc sonde + Studio) et transitions annoncées.
22
+ * Cf `project_resilience_no_silent_degradation`.
23
+ *
24
+ * Ce qui est jeté est le message **nouveau**, pas le plus ancien : contrairement au
25
+ * WS (drop latest-wins sur une file qu'on possède), ce qui est déjà remis au client
26
+ * réseau ne peut plus être repris.
27
+ */
28
+ import type { IBackplaneQueueInfo } from "../../interfaces/IBackplane.js";
29
+ /** Sévérité d'une transition de la file — mappée telle quelle sur le syslog. */
30
+ export type BackplaneNoticeSeverity = "WARNING" | "INFO";
31
+ /** Annonce d'une transition (saturation / retour à la normale). */
32
+ export type BackplaneNotice = (message: string, severity: BackplaneNoticeSeverity) => void;
33
+ /**
34
+ * Seuil par défaut d'octets en vol vers le bus : **8 MiB**, aligné sur le seuil de
35
+ * coupure d'un slow-consumer WS. Un bus sain acquitte en quelques millisecondes
36
+ * (3 ms mesurés cross-pod) : 8 MiB d'arriéré, c'est déjà trois ordres de grandeur
37
+ * au-dessus du régime nominal — le pod a un problème bien avant d'y arriver.
38
+ */
39
+ export declare const DEFAULT_MAX_QUEUE_BYTES: number;
40
+ /**
41
+ * File d'envoi bornée : décide d'admettre ou de jeter une publication, puis suit
42
+ * son acquittement pour rendre la place.
43
+ *
44
+ * Un transport **synchrone** (bus mémoire des tests, IPC) ne rend pas de promesse :
45
+ * il n'a donc pas de file, la garde reste alors inerte et ne jette jamais rien.
46
+ */
47
+ export declare class BackplanePublishQueue {
48
+ #private;
49
+ /**
50
+ * @param maxBytes - seuil d'octets en vol ; `0` (ou négatif) = illimité.
51
+ * @param notice - annonce des transitions ; omis = silencieux (tests unitaires).
52
+ */
53
+ constructor(maxBytes?: number, notice?: BackplaneNotice | null);
54
+ /**
55
+ * Admet ou jette une publication, et suit son acquittement.
56
+ *
57
+ * L'admission regarde l'état de la file **avant** l'envoi, pas la taille du
58
+ * message : une charge plus grosse que le seuil part quand la file est vide
59
+ * (sinon elle ne partirait jamais — famine silencieuse), quitte à dépasser
60
+ * transitoirement. Même règle que le drop WS, qui teste `bufferedAmount` seul.
61
+ *
62
+ * @param bytes - taille de la charge sérialisée.
63
+ * @param emit - envoi réel ; appelé UNIQUEMENT si la publication est admise.
64
+ * Son retour est suivi s'il s'agit d'une promesse (transport asynchrone).
65
+ * @returns `true` si la publication est partie, `false` si elle a été jetée.
66
+ */
67
+ send(bytes: number, emit: () => unknown): boolean;
68
+ /** Compteurs pour la carte d'identité du backplane (sonde + Studio). */
69
+ describe(): IBackplaneQueueInfo;
70
+ }
71
+ export default BackplanePublishQueue;
@@ -0,0 +1,90 @@
1
+ import type { ClusterProbeFacet } from "nodefony";
2
+ import type { IRealtimeHealth, IRealtimeClusterHealth } from "../../interfaces/IRealtimeProbe.js";
3
+ /**
4
+ * Transport IPC de la sonde cluster côté worker — abstrait `process.send` / la réception
5
+ * des snapshots du master, derrière un seam **injectable** (même pattern que
6
+ * `IClusterBackplaneTransport`). Permet de tester le report + le cache **sans forker**.
7
+ */
8
+ export interface IClusterProbeTransport {
9
+ /** Émet une remontée de sonde vers le master (worker → master). No-op hors cluster. */
10
+ send(report: unknown): void;
11
+ /** Enregistre le récepteur des snapshots agrégés du master (master → worker). */
12
+ onReceive(cb: (msg: unknown) => void): void;
13
+ }
14
+ /** Transport de production : `process.send` + `process.on("message")`. */
15
+ export declare const processProbeTransport: IClusterProbeTransport;
16
+ /**
17
+ * Consolide N santés per-instance en une vue **POD** ({@link IRealtimeClusterHealth}).
18
+ * Pur (aucune I/O) : somme les scalaires ; la backpressure `maxBufferedAmount` est un MAX
19
+ * (le pire worker), `totalBufferedAmount`/`slowConsumers` des sommes.
20
+ *
21
+ * @param instances - santés per-instance (chaque worker, snapshot master).
22
+ * @param ts - horodatage du snapshot (défaut maintenant).
23
+ */
24
+ export declare function mergeClusterHealth(instances: IRealtimeHealth[], ts?: number): IRealtimeClusterHealth;
25
+ /**
26
+ * Client de sonde cluster **côté worker** (Phase 4c) — le pendant de
27
+ * {@link ClusterProbeAggregator} (master). Deux rôles :
28
+ * - **report** : envoie périodiquement sa santé per-instance au master (`CLUSTER_PROBE_KIND`) ;
29
+ * - **cache** : reçoit le snapshot agrégé du master (`CLUSTER_PROBE_SNAPSHOT_KIND`) et le garde
30
+ * → n'importe quel worker sert la vue POD en O(1) (modèle push : 0 latence de requête).
31
+ *
32
+ * **Bypass total quand désactivé** : ce client n'est instancié/`start()` QUE par le module
33
+ * Framework en worker de cluster AVEC la sonde activée. Sinon (mono-process, ou sonde coupée)
34
+ * il n'existe pas → aucun timer, aucun listener, aucun IPC, et {@link clusterProbeHealth}
35
+ * renvoie `null` → l'endpoint santé retombe sur la vue per-instance. Aucun coût « au cas où ».
36
+ *
37
+ * Perf : 1 timer `unref` (report) + 1 listener IPC. Le report sérialise 1 santé / intervalle
38
+ * (≥ 1 s, jamais hot path). SERVEUR uniquement.
39
+ */
40
+ export declare class ClusterProbeClient {
41
+ #private;
42
+ constructor(transport?: IClusterProbeTransport, intervalMs?: number);
43
+ /**
44
+ * Démarre le report périodique + l'écoute des snapshots. Idempotent. 1ᵉʳ report immédiat.
45
+ *
46
+ * @param buildOwn - fournit la santé per-instance courante (capturée à chaque tick).
47
+ */
48
+ start(buildOwn: () => IRealtimeHealth): void;
49
+ /**
50
+ * Demande au master d'(arrêter d')enrichir le worker `pid` sur une **facette** (drill-down).
51
+ * Émis par le worker qui tient la connexion navigateur quand un client subscribe/unsubscribe
52
+ * le canal `nodefony:supervision@<pid>` (facette `"process"`) ou `nodefony:orm:rich@<pid>` (facette
53
+ * `"orm"`). No-op hors cluster (`send` no-op).
54
+ *
55
+ * @param pid - worker ciblé (identité de la vue pod).
56
+ * @param enable - `true` = activer la sonde riche sur ce worker, `false` = la couper.
57
+ * @param facet - quelle sonde riche cibler (défaut `"process"`).
58
+ */
59
+ requestEnrich(pid: number, enable: boolean, facet?: ClusterProbeFacet): void;
60
+ /** Vue POD agrégée, ou `null` tant qu'aucun snapshot n'est reçu (cold start). */
61
+ getClusterHealth(): IRealtimeClusterHealth | null;
62
+ /** Arrête le report et purge le cache. Idempotent. Libère la sonde riche (drill). */
63
+ stop(): void;
64
+ }
65
+ /** Branche le client de sonde cluster du process (worker). Remplace un éventuel précédent. */
66
+ export declare function setClusterProbeClient(client: ClusterProbeClient): ClusterProbeClient;
67
+ /**
68
+ * Vue POD agrégée si la sonde cluster est branchée ET a reçu un snapshot, sinon `null`.
69
+ * `null` ⇒ l'endpoint santé sert la vue per-instance (mono-process / sonde désactivée /
70
+ * cold start). Lecture pure, jamais throw.
71
+ */
72
+ export declare function clusterProbeHealth(): IRealtimeClusterHealth | null;
73
+ /**
74
+ * Demande au master d'(arrêter d')enrichir le worker `pid` sur une **facette** (drill-down
75
+ * Phase 2). Facettes indépendantes : `"process"` (supervision) et `"orm"` (drill ORM).
76
+ *
77
+ * @param pid - worker ciblé.
78
+ * @param enable - activer/couper la sonde riche.
79
+ * @param facet - quelle sonde riche cibler (défaut `"process"`).
80
+ * @returns `true` si la sonde cluster est branchée (worker de cluster) → l'ordre est émis ;
81
+ * `false` en mono-process / sonde désactivée (pas de drill cross-worker possible → le
82
+ * consommateur retombe sur la sonde riche locale du process courant).
83
+ */
84
+ export declare function clusterProbeRequestEnrich(pid: number, enable: boolean, facet?: ClusterProbeFacet): boolean;
85
+ /**
86
+ * Santé d'UN worker `pid` extraite du dernier snapshot pod (avec sa sonde riche si le
87
+ * drill est actif), ou `null` (mono-process, pid inconnu, ou cold start). Lecture pure.
88
+ */
89
+ export declare function clusterProbeInstance(pid: number): IRealtimeHealth | null;
90
+ export default ClusterProbeClient;
@@ -0,0 +1,3 @@
1
+ import type { IRealtimeToken } from "../../interfaces/IRealtimeToken.js";
2
+ export declare const ANONYMOUS_REALTIME_TOKEN: IRealtimeToken;
3
+ export default ANONYMOUS_REALTIME_TOKEN;
@@ -0,0 +1,18 @@
1
+ import type { IAdminApi } from "nodefony";
2
+ import type { IRealtimeHealth, IRealtimeClusterHealth } from "../../interfaces/IRealtimeProbe.js";
3
+ export declare function buildOwnHealth(): IRealtimeHealth;
4
+ /**
5
+ * Construit la **santé de la socket** servie par l'endpoint/le canal `nodefony:socket` :
6
+ * la **vue POD agrégée** si la sonde cluster est branchée et a reçu un snapshot du master
7
+ * (Phase 4c, push), sinon la **vue per-instance** (mono-process, sonde désactivée, ou cold
8
+ * start). Lecture pure, jamais throw. Async pour matcher la signature des tickers broker.
9
+ *
10
+ * @returns {@link IRealtimeClusterHealth} (cluster) ou {@link IRealtimeHealth} (per-instance).
11
+ */
12
+ export declare function buildRealtimeHealth(): Promise<IRealtimeHealth | IRealtimeClusterHealth>;
13
+ /**
14
+ * Construit le producteur `IAdminApi` de la socket (namespace `"realtime"`).
15
+ *
16
+ * @returns le contrat admin, prêt à `broker.register()`.
17
+ */
18
+ export declare function createRealtimeAdminApi(): IAdminApi;