@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.
- package/LICENSE +544 -0
- package/README.md +207 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +218 -0
- package/dist/nodefony/config/config.js +63 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
- package/dist/nodefony/interfaces/IBackplane.js +1 -0
- package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
- package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
- package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
- package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
- package/dist/nodefony/src/backplane/envelope.js +109 -0
- package/dist/nodefony/src/backplane/originId.js +35 -0
- package/dist/nodefony/src/backplane/publishQueue.js +96 -0
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
- package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
- package/dist/nodefony/src/server/RealtimeController.js +601 -0
- package/dist/nodefony/src/server/RealtimeHub.js +926 -0
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
- package/dist/nodefony/src/server/deniedDetail.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +105 -0
- package/dist/nodefony/src/server/welcomeEnv.js +25 -0
- package/dist/nodefony/src/service/RealtimeService.js +240 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
- package/dist/nodefony/testing/index.js +163 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/nodefony/config/config.d.ts +43 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
- package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
- package/dist/types/nodefony/interfaces/index.d.ts +18 -0
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
- package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
- package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
- package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
- package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
- package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
- package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +169 -0
- package/docs/actions.md +573 -0
- package/docs/architecture.md +889 -0
- package/docs/configuration.md +803 -0
- package/docs/cookbook-chat.md +1619 -0
- package/docs/couverture-bancs.md +139 -0
- package/docs/index.md +372 -0
- package/docs/observabilite.md +592 -0
- package/docs/protocole.md +518 -0
- package/docs/securite.md +845 -0
- package/docs/vocabulaire.md +845 -0
- 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,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;
|