@nodefony/realtime 10.0.0-beta.5 → 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.
Files changed (96) hide show
  1. package/README.md +55 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +91 -12
  4. package/dist/nodefony/config/config.js +7 -0
  5. package/dist/nodefony/decorators/realtimeDecorators.js +341 -43
  6. package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
  7. package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
  8. package/dist/nodefony/src/backplane/RedisBackplane.js +146 -41
  9. package/dist/nodefony/src/backplane/controlOrder.js +86 -0
  10. package/dist/nodefony/src/backplane/envelope.js +505 -27
  11. package/dist/nodefony/src/backplane/ingress.js +93 -0
  12. package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
  13. package/dist/nodefony/src/backplane/publishQueue.js +97 -3
  14. package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
  15. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
  16. package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
  17. package/dist/nodefony/src/server/RealtimeConnection.js +135 -0
  18. package/dist/nodefony/src/server/RealtimeController.js +409 -107
  19. package/dist/nodefony/src/server/RealtimeHub.js +1158 -111
  20. package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
  21. package/dist/nodefony/src/server/admissionBudget.js +176 -0
  22. package/dist/nodefony/src/server/channelPolicyRegistry.js +95 -0
  23. package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
  24. package/dist/nodefony/src/server/channelPresence.js +488 -0
  25. package/dist/nodefony/src/server/connectionAdmission.js +242 -0
  26. package/dist/nodefony/src/server/contractRoutes.js +75 -0
  27. package/dist/nodefony/src/server/deniedDetail.js +10 -3
  28. package/dist/nodefony/src/server/frameAdmission.js +160 -0
  29. package/dist/nodefony/src/server/presenceBus.js +486 -0
  30. package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
  31. package/dist/nodefony/src/server/remotePresence.js +541 -0
  32. package/dist/nodefony/src/server/subscribedAck.js +13 -1
  33. package/dist/nodefony/src/server/syslogUplink.js +3 -4
  34. package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
  35. package/dist/nodefony/src/service/RealtimeService.js +51 -1
  36. package/dist/nodefony/src/service/publicOrigin.js +40 -0
  37. package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
  38. package/dist/nodefony/testing/backplaneConformance.js +347 -0
  39. package/dist/nodefony/testing/index.js +43 -12
  40. package/dist/nodefony/testing/presenceClient.js +54 -0
  41. package/dist/types/index.d.ts +39 -7
  42. package/dist/types/nodefony/config/config.d.ts +9 -0
  43. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +198 -17
  44. package/dist/types/nodefony/interfaces/IBackplane.d.ts +46 -51
  45. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
  46. package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
  47. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
  48. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
  49. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
  50. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  52. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
  53. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +89 -18
  54. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +75 -0
  55. package/dist/types/nodefony/src/backplane/envelope.d.ts +77 -0
  56. package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
  57. package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
  58. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
  59. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
  60. package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
  61. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +20 -0
  62. package/dist/types/nodefony/src/server/RealtimeController.d.ts +110 -12
  63. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +448 -42
  64. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
  65. package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
  66. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +41 -0
  67. package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
  68. package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
  69. package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
  70. package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
  71. package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
  72. package/dist/types/nodefony/src/server/frameAdmission.d.ts +66 -0
  73. package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
  74. package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
  75. package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
  76. package/dist/types/nodefony/src/server/subscribedAck.d.ts +10 -0
  77. package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
  78. package/dist/types/nodefony/src/service/RealtimeService.d.ts +17 -2
  79. package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
  80. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
  81. package/dist/types/nodefony/testing/backplaneConformance.d.ts +78 -0
  82. package/dist/types/nodefony/testing/index.d.ts +13 -2
  83. package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
  84. package/docs/actions.md +420 -18
  85. package/docs/architecture.md +101 -11
  86. package/docs/cadence-et-contre-pression.md +87 -11
  87. package/docs/canaux-a-membres.md +277 -0
  88. package/docs/cas-d-usage.md +1703 -0
  89. package/docs/configuration.md +175 -19
  90. package/docs/cookbook-chat.md +701 -224
  91. package/docs/index.md +11 -1
  92. package/docs/observabilite.md +97 -21
  93. package/docs/protocole.md +142 -33
  94. package/docs/securite.md +347 -41
  95. package/docs/vocabulaire.md +13 -12
  96. package/package.json +16 -13
@@ -1,4 +1,12 @@
1
- import { JsonRpcPeer, isPlatformChannel } from "nodefony";
1
+ import { JsonRpcPeer, isPlatformChannel, type IFrameCodec, type IFrameCodecSet, type RealtimeFrameData } from "nodefony";
2
+ import { RealtimeConnection } from "./RealtimeConnection.js";
3
+ import { type IChannelPresenceView } from "./channelPresence.js";
4
+ import type { IRealtimeConnection } from "../../interfaces/IRealtimeConnection.js";
5
+ import type { IChannelAdmission, IChannelPolicies, UserChannelPayload } from "nodefony";
6
+ import { AdmissionBudget, type IAdmissionLimits } from "./admissionBudget.js";
7
+ import type { IChannelAdmissionRule } from "./connectionAdmission.js";
8
+ import type { IRealtimeConnectionState } from "../../interfaces/IRealtimeConnectionState.js";
9
+ import type { ContractServerEmit, EventNames, EventPayload, IRealtimeContract } from "nodefony";
2
10
  import type { RealtimePublish } from "../../interfaces/IRealtimeController.js";
3
11
  import type { IClientLogsLimits } from "../../interfaces/IClientLogsLimits.js";
4
12
  import type { IChannelPolicy } from "../../interfaces/IChannelPolicy.js";
@@ -26,8 +34,21 @@ export type OriginGuard = (origin: string | undefined) => boolean;
26
34
  * `@nodefony/security` au boot via {@link RealtimeService.setFrameAuthorizer}.
27
35
  * Doctrine SYNC stricte (cf `JsonRpcPeerOptions.beforeDispatch`) : un `await` par
28
36
  * frame coûterait une microtask et sérialiserait le pipeline RPC du peer.
37
+ *
38
+ * `judgesWrite` : le verrou sait juger une politique d'ÉCRITURE distincte de la
39
+ * lecture (ADR-0022 §6, décision 1 quater). Un verrou qui ne le déclare pas ne
40
+ * lirait que la lecture, et laisserait ouverte une écriture plus stricte : le
41
+ * hub refuse alors de démarrer dès qu'une telle écriture est déclarée.
42
+ *
43
+ * `explain` : la phrase qui dit POURQUOI une trame a été refusée, quand le
44
+ * verrou sait la rédiger (`null` sinon). Lu après un refus seulement
45
+ * ({@link RealtimeHub.explainDenial}) — jamais sur le trajet d'une trame
46
+ * acceptée. Elle va au journal du serveur, et au client hors production.
29
47
  */
30
- export type FrameAuthorizer = (frame: unknown, token: IRealtimeToken) => boolean;
48
+ export type FrameAuthorizer = ((frame: unknown, token: IRealtimeToken) => boolean) & {
49
+ readonly judgesWrite?: true;
50
+ readonly explain?: (frame: unknown, token: IRealtimeToken) => string | null;
51
+ };
31
52
  /**
32
53
  * Seuil d'alerte slow-consumer (octets de `bufferedAmount`). Au-delà, la connexion
33
54
  * est comptée comme « lente » par la sonde (PAS encore de drop/close — la sonde MESURE
@@ -108,18 +129,62 @@ export interface IRevocableConnection {
108
129
  * Sink d'un canal : pousse une charge vers UNE connexion abonnée (son `peer.notify`).
109
130
  * 1 sink = 1 connexion. Le hub fan-out la charge à tous les sinks d'un canal.
110
131
  *
111
- * `serialized` n'est fourni que si le canal a déclaré un {@link ChannelSerializer}
112
- * ET qu'il a plusieurs abonnés : la frame ayant été sérialisée une fois pour tous,
113
- * le sink peut l'envoyer telle quelle au lieu de refaire le travail. Absent, le
114
- * sink se débrouille comme avant — un sink historique à un seul paramètre ignore
115
- * simplement l'argument.
132
+ * `frame` n'est fourni que si le canal a déclaré un {@link ChannelFrameBuilder} ET
133
+ * qu'il a plusieurs abonnés : la frame est la MÊME pour tous, le sink en demande
134
+ * l'encodage pour le codec de SA connexion ({@link ChannelFrame.encodeFor}) au lieu
135
+ * de refaire le travail. Absent, le sink se débrouille comme avant — un sink à un
136
+ * seul paramètre ignore simplement l'argument.
137
+ */
138
+ export type ChannelSink = (payload: unknown, frame?: ChannelFrame) => void;
139
+ /**
140
+ * Fabrique la frame protocolaire d'un canal (l'objet, pas ses octets), fournie par
141
+ * l'abonné : le hub ignore tout du protocole. Appelée **au plus une fois par
142
+ * publication**, jamais par abonné ; l'encodage, lui, se fait une fois par codec.
116
143
  */
117
- export type ChannelSink = (payload: unknown, serialized?: string) => void;
144
+ export type ChannelFrameBuilder = (payload: unknown) => unknown;
118
145
  /**
119
- * Sérialiseur de la frame d'un canal, fourni par l'abonné (le hub ignore tout du
120
- * protocole). Appelé **au plus une fois par publication**, jamais par abonné.
146
+ * Frame d'UNE publication diffusée à plusieurs abonnés, encodée **au plus une fois
147
+ * par codec** (ADR-0022 §2) : un canal suivi par des connexions texte et CBOR paie
148
+ * deux encodages, jamais un par connexion. L'encodage est paresseux — un codec
149
+ * qu'aucun abonné n'utilise ne coûte rien.
150
+ *
151
+ * Deux places suffisent : un hub sert au plus son codec texte et son codec
152
+ * binaire. Un troisième codec reste servi, sans mise en cache.
121
153
  */
122
- export type ChannelSerializer = (payload: unknown) => string;
154
+ export declare class ChannelFrame {
155
+ private readonly message;
156
+ private codecA;
157
+ private dataA;
158
+ private codecB;
159
+ private dataB;
160
+ private claimedA;
161
+ private claimedB;
162
+ /** @param message - la frame protocolaire, telle que rendue par le {@link ChannelFrameBuilder}. */
163
+ constructor(message: unknown);
164
+ /**
165
+ * Rend la frame encodée pour un codec, en l'encodant à la première demande.
166
+ *
167
+ * @param codec - le codec de la connexion qui va l'écrire.
168
+ * @returns la trame à écrire telle quelle, ou `undefined` si la frame n'est pas
169
+ * encodable : l'abonné qui en {@link ChannelFrame.claimFailure | réclame}
170
+ * l'échec reprend son chemin habituel, qui porte le filet (journal).
171
+ */
172
+ encodeFor(codec: IFrameCodec): RealtimeFrameData | undefined;
173
+ /**
174
+ * Réclame l'échec d'encodage de cette frame pour un codec : `true` au PREMIER
175
+ * abonné qui le demande, `false` ensuite.
176
+ *
177
+ * Un seul abonné reprend alors son chemin habituel, dont le filet journalise
178
+ * l'échec ; les autres sautent la trame, qu'ils ne pourraient pas encoder non
179
+ * plus. Sans cela, une charge non encodable — un `$bytes` forgé par un client,
180
+ * une valeur cyclique — coûtait un encodage et une ligne de journal PAR abonné.
181
+ * Un codec hors des deux places réclame à chaque fois : il n'est pas retenu.
182
+ *
183
+ * @param codec - le codec dont {@link ChannelFrame.encodeFor} a rendu `undefined`.
184
+ * @returns `true` si l'appelant doit dire l'échec.
185
+ */
186
+ claimFailure(codec: IFrameCodec): boolean;
187
+ }
123
188
  /**
124
189
  * Fabrique le provider PARTAGÉ d'un canal (listener/ticker qui pousse via `publish`).
125
190
  * Appelée UNE fois, au 1ᵉʳ abonné. Renvoie son `dispose` (appelé au dernier
@@ -128,6 +193,17 @@ export type ChannelSerializer = (payload: unknown) => string;
128
193
  * a déclenché la création (elle peut fermer alors que le provider partagé survit).
129
194
  */
130
195
  export type ChannelFactory = (channel: string, publish: RealtimePublish) => (() => void) | null;
196
+ /**
197
+ * Qui a déclaré une sorte de politique sous un nom, et laquelle — retenu au démarrage pour refuser deux classes en désaccord sur un même nom.
198
+ */
199
+ export interface IChannelPolicyClaim {
200
+ /** Constructeur du contrôleur qui a déclaré le premier. */
201
+ readonly owner: object;
202
+ /** Son nom, pour le message de refus. */
203
+ readonly ownerName: string;
204
+ /** La politique déclarée. */
205
+ readonly policy: IChannelPolicy;
206
+ }
131
207
  /**
132
208
  * RealtimeHub — broker temps réel **PAR INSTANCE** (1 pod = 1 process = 1 hub local).
133
209
  *
@@ -157,6 +233,12 @@ export type ChannelFactory = (channel: string, publish: RealtimePublish) => (()
157
233
  */
158
234
  export declare class RealtimeHub {
159
235
  #private;
236
+ /**
237
+ * Budget des admissions de l'exemplaire (appels d'`allow` en vol, coupe-circuit,
238
+ * `allowTimedOutTotal`/`allowSaturatedTotal`) — partagé par les files de toutes
239
+ * les connexions. Bornes posées par {@link setAdmissionLimits} au boot.
240
+ */
241
+ readonly admissionBudget: AdmissionBudget;
160
242
  /**
161
243
  * Abonnement demandé par une **connexion cliente** — même chose que
162
244
  * {@link subscribe}, plus le plancher des canaux de plateforme.
@@ -171,9 +253,11 @@ export declare class RealtimeHub {
171
253
  * sécurité en pose un, c'est lui qui décide (avec les rôles, ce que le hub ne
172
254
  * sait pas faire) et le plancher s'efface.
173
255
  *
256
+ * @param member - la connexion qui s'abonne : inscrite au registre du canal,
257
+ * elle devient un membre identifiable ({@link channelMembers}).
174
258
  * @returns `true` si abonné, `false` si canal inconnu ou fermé par le plancher.
175
259
  */
176
- subscribeClient(channel: string, sink: ChannelSink, factory: ChannelFactory, serialize?: ChannelSerializer): boolean;
260
+ subscribeClient(channel: string, sink: ChannelSink, factory: ChannelFactory, buildFrame?: ChannelFrameBuilder, member?: RealtimeConnection): boolean;
177
261
  /**
178
262
  * Abonne un consommateur qui **répond du canal** : il apporte la `factory`, donc
179
263
  * la réponse à « ce canal existe-t-il, et qui le produit ? ». Le provider partagé
@@ -187,7 +271,7 @@ export declare class RealtimeHub {
187
271
  *
188
272
  * @returns `true` si abonné, `false` si canal inconnu (`factory` a renvoyé `null`).
189
273
  */
190
- subscribe(channel: string, sink: ChannelSink, factory: ChannelFactory, serialize?: ChannelSerializer): boolean;
274
+ subscribe(channel: string, sink: ChannelSink, factory: ChannelFactory, buildFrame?: ChannelFrameBuilder): boolean;
191
275
  /**
192
276
  * **Écoute passive** — un service du serveur reçoit ce qui passe sur un canal
193
277
  * sans prétendre le produire. C'est la porte de {@link ServerRealtimeSocket} :
@@ -213,6 +297,131 @@ export declare class RealtimeHub {
213
297
  * et retire le canal (libère timers/listeners). No-op si non abonné.
214
298
  */
215
299
  unsubscribe(channel: string, sink: ChannelSink): void;
300
+ /**
301
+ * Présence d'un canal vue de CE pod (ADR-0025 P3) — `count`, `unnamed`, `members` (toujours, quel que soit le seuil de la trame), `partial`.
302
+ *
303
+ * La voie de l'observateur qui lit sans figurer (modérateur, tableau de
304
+ * bord). Chemin froid : une copie à chaque appel.
305
+ *
306
+ * @param channel - nom exact du canal.
307
+ * @returns la vue (vide si personne n'est là), ou `null` si la politique du
308
+ * canal ne déclare pas `presence`.
309
+ */
310
+ presence(channel: string): IChannelPresenceView | null;
311
+ /**
312
+ * Arrêt propre de la présence répartie (ADR-0025 P5) — le dernier lot part, puis une preuve de vie FINALE : les pairs retirent aussitôt les présents de ce serveur, sans attendre le délai d'oubli. Plus rien ne part ensuite ; idempotent, sans effet sans bus.
313
+ *
314
+ * Appelé par le module à l'arrêt du noyau (`onTerminate`), AVANT que le bus ne
315
+ * se ferme. Les connexions qui se ferment ensuite ne publient plus rien : la
316
+ * preuve de vie finale les a déjà retirées chez les pairs.
317
+ */
318
+ endPresence(): void;
319
+ /**
320
+ * Un abonnement vient d'être acquitté : sur un canal à présence, la liste complète part vers CETTE connexion, puis les changements en attente partent si la fenêtre le permet.
321
+ *
322
+ * Appelé par le contrôleur APRÈS `realtime:subscribed`, à chaque (re-)`subscribe`
323
+ * — aucune borne : la contre-pression peut avoir jeté la liste d'avant sans
324
+ * le dire, et c'est le re-`subscribe` qui répare (ADR-0025 P2 bis). La liste
325
+ * est encodée une fois par envoi de changements et par codec, puis servie
326
+ * telle quelle.
327
+ *
328
+ * @param channel - nom exact du canal acquitté.
329
+ * @param connection - la connexion abonnée.
330
+ */
331
+ presenceSubscribed(channel: string, connection: RealtimeConnection): void;
332
+ /**
333
+ * Les connexions abonnées à un canal par un client — **local à ce pod** : les membres servis par un autre exemplaire n'y figurent pas.
334
+ *
335
+ * Les écouteurs et services du serveur (`listen`, `subscribe`) ne sont pas
336
+ * des membres. Chemin froid : une copie à chaque appel.
337
+ *
338
+ * @param channel - nom exact du canal.
339
+ * @returns les membres de ce pod, vide si le canal n'en a aucun ici.
340
+ */
341
+ channelMembers(channel: string): IRealtimeConnection[];
342
+ /**
343
+ * Abonne d'office une connexion à un canal — **local à ce pod** : seule une connexion acceptée par ce processus peut l'être.
344
+ *
345
+ * Même porte qu'un `subscribe` du client (ADR-0022 §6 décision 2) : la
346
+ * politique fixe du canal (rôles, scopes, plancher de plateforme), puis sa
347
+ * règle `allow` s'il en déclare une. Un refus n'abonne pas, et seul
348
+ * l'appelant l'apprend ; jamais un abonnement qui contourne `allow` — le
349
+ * client rejoue ses abonnements à chaque reconnexion, et ce rejeu passe par
350
+ * `allow` : l'application écrit donc l'appartenance là où `allow` la lit.
351
+ *
352
+ * Le client est notifié par `realtime:subscribed { channel, imposed: true }`
353
+ * et inscrit le canal dans ses abonnements ; il peut le quitter.
354
+ *
355
+ * Plusieurs exemplaires : la requête qui décide doit atteindre le processus
356
+ * qui tient la socket — la passer par la socket elle-même (`@RealtimeInbound`,
357
+ * qui reçoit la connexion), ou faire autoriser l'entrée par `allow` (registre
358
+ * partagé) et laisser le navigateur s'abonner.
359
+ *
360
+ * @param channel - nom exact du canal.
361
+ * @param connection - la connexion à abonner (reçue par un gestionnaire `@RealtimeInbound`, ou lue dans {@link channelMembers}).
362
+ * @returns `true` une fois l'abonnement acquitté ; `false` si refusé, si la
363
+ * connexion est fermée ou inconnue de ce processus, ou si le client s'en
364
+ * désabonne avant le verdict.
365
+ */
366
+ subscribeConnection(channel: string, connection: IRealtimeConnection): Promise<boolean>;
367
+ /**
368
+ * Retire une connexion d'un canal — **local à ce pod** ; elle reçoit `realtime:closed { reason: "evicted" }`.
369
+ *
370
+ * ⚠️ **Éjecter coupe la connexion du canal, seul `allow` exclut**
371
+ * (ADR-0022 §6 décision 3). Le client ne rejoue pas un canal dont il a été
372
+ * éjecté, mais c'est une politesse : une page rechargée refait `subscribe`.
373
+ * Sur un canal à `allow`, l'éjecté revient si l'application n'a pas retiré
374
+ * l'appartenance là où `allow` la lit ; sur un canal sans `allow`, sa
375
+ * politique fixe le laisse rentrer.
376
+ *
377
+ * @param channel - nom exact du canal.
378
+ * @param connection - la connexion à retirer.
379
+ * @returns `false` si la connexion n'est pas membre du canal, est fermée, ou
380
+ * inconnue de ce processus — jamais un geste muet.
381
+ */
382
+ evictConnection(channel: string, connection: IRealtimeConnection): boolean;
383
+ /**
384
+ * Termine un canal pour tous ses membres, **sur tous les serveurs** : chacun reçoit `realtime:closed { reason: "ended" }`, et le producteur est libéré.
385
+ *
386
+ * **Traverse les pods** : avec un backplane branché, l'ordre part sur le bus
387
+ * (`PLATFORM_BUS_CHANNELS.control`, scellé comme toute publication) et chaque
388
+ * serveur l'applique à SES membres. Livraison « au plus une fois » du bus :
389
+ * un pod qui manque l'ordre, ou qui démarre après, est rattrapé par la
390
+ * fabrique, qui rend `null` pour une instance terminée. Un canal de la
391
+ * plateforme (`nodefony:`) reste local — il décrit un pod, pas une partie.
392
+ *
393
+ * Les écouteurs du serveur (`listen`) restent inscrits, sur un canal redevenu
394
+ * PASSIF : le prochain `subscribe` d'un client rejoue la fabrique, et c'est
395
+ * elle qui tranche. **Seule la fabrique clôt un canal** (ADR-0021 S4) — le
396
+ * hub ne garde aucune marque « fermé » : une fabrique qui rend encore un
397
+ * producteur pour ce nom le rouvre. D'où la règle d'écriture d'un canal à
398
+ * durée de vie : son nom porte l'identifiant de l'instance
399
+ * (`room:42:<partie>`), et la fabrique rend `null` pour une instance terminée.
400
+ *
401
+ * @param channel - nom exact du canal.
402
+ * @returns le nombre de membres de CE pod à qui la fin a été notifiée — les
403
+ * autres serveurs l'appliquent sans compte rendu.
404
+ */
405
+ closeChannel(channel: string): number;
406
+ /**
407
+ * Éjecte un compte d'un canal, **sur tous les serveurs** : chacune de ses connexions membres reçoit `realtime:closed { reason: "evicted" }`.
408
+ *
409
+ * Tous les onglets du compte sont visés, où qu'ils soient servis ; l'ordre
410
+ * traverse les pods comme {@link closeChannel}. ⚠️ **Éjecter coupe, seul
411
+ * `allow` exclut** (ADR-0022 §6 décision 3) : pour que le compte ne revienne
412
+ * pas, l'application retire l'appartenance là où `allow` la lit.
413
+ *
414
+ * ⚠️ Sur un bus PARTAGÉ, un tiers qui y écrit peut rejouer un ordre capté :
415
+ * sur un canal durable, il recouperait le compte à chaque réadmission (risque
416
+ * accepté et borné, ADR-0022 §6 d4) — bus privé exigé ; changer le secret du
417
+ * bus rend caducs les ordres déjà captés.
418
+ *
419
+ * @param channel - nom exact du canal.
420
+ * @param user - identifiant du compte (`IRealtimeConnection.userId`).
421
+ * @returns le nombre de connexions de CE pod éjectées. Un nom vide ou de plus
422
+ * de 1 024 caractères n'éjecte rien et ne part pas sur le bus.
423
+ */
424
+ evictUser(channel: string, user: string): number;
216
425
  /**
217
426
  * Publie une charge sur un canal : **fan-out local** **+** propagation conditionnelle
218
427
  * au {@link IBackplane} (autres workers/pods). C'est le point d'entrée des providers
@@ -226,8 +435,29 @@ export declare class RealtimeHub {
226
435
  *
227
436
  * Anti-boucle : un message **reçu** du backplane ne repasse PAS par ici (il appelle
228
437
  * `publishLocal` directement) → aucune ré-émission cross-process, pas de tempête.
438
+ *
439
+ * @param payload - objet, tableau ou octets ; omise = signal sans charge.
440
+ * @throws TypeError si la charge est un scalaire ou `null` ({@link isRealtimePayload},
441
+ * ADR-0021 S1) — un scalaire se publie dans un objet (`{ value }`).
442
+ */
443
+ publish(channel: string, payload?: object): void;
444
+ /**
445
+ * Livre une charge à UNE identité — tous ses onglets, tous les serveurs, et personne d'autre (ADR-0022 §6, D1).
446
+ *
447
+ * Publie sur son canal personnel `user:<userId>` : seuls les onglets de cette
448
+ * identité ont pu s'y abonner (politique intégrée, admission par égalité de
449
+ * chaîne), et le préfixe traverse le bus dès qu'un bus est branché. Rien à
450
+ * déclarer côté application. Personne n'écoute ? La charge est perdue, sans
451
+ * erreur : c'est une diffusion, pas une boîte aux lettres.
452
+ *
453
+ * @param userId - l'`id` STABLE du compte (`RealtimeIdentity.id`), jamais le
454
+ * login.
455
+ * @param payload - objet, tableau ou octets ; omise = signal sans charge.
456
+ * @throws TypeError si la charge est un scalaire ou `null` (cf {@link publish}).
229
457
  */
230
- publish(channel: string, payload: unknown): void;
458
+ publishToUser(userId: string, payload?: object): void;
459
+ /** Préfixes déclarés diffusables entre serveurs, dans l'ordre de déclaration — lu par la ligne de démarrage. */
460
+ broadcastPrefixes(): readonly string[];
231
461
  /**
232
462
  * Fan-out d'une charge aux **seuls abonnés locaux** d'un canal (process courant).
233
463
  * NE propage PAS au backplane → c'est la voie d'**ingress** d'un message venu d'un
@@ -269,7 +499,11 @@ export declare class RealtimeHub {
269
499
  * sur le chemin chaud). À équilibrer par {@link unregisterConnection} au close.
270
500
  */
271
501
  registerConnection(conn: IRealtimeConnProbe): void;
272
- /** Retire une connexion du registre de la sonde (au close). No-op si absente. */
502
+ /**
503
+ * Retire une connexion du registre de la sonde (au close) et verse ses cumuls à
504
+ * ceux des connexions fermées : les totaux de {@link probe} restent monotones.
505
+ * No-op si absente — une double sortie ne compte pas deux fois.
506
+ */
273
507
  unregisterConnection(conn: IRealtimeConnProbe): void;
274
508
  /**
275
509
  * Inscrit une connexion à identité RÉVOCABLE au registre de
@@ -361,6 +595,30 @@ export declare class RealtimeHub {
361
595
  * `defineRealtimeConfig().slowConsumer.bytes` (sinon la clé de config serait morte).
362
596
  */
363
597
  setSlowConsumerBytes(bytes: number): void;
598
+ /**
599
+ * Les codecs de frame du serveur (ADR-0022 §2) : le texte, et le codec binaire
600
+ * accordé aux clients qui l'offrent — `null` tant que la configuration n'en
601
+ * active aucun. Lu une fois par connexion, au handshake, jamais par trame.
602
+ */
603
+ get frameCodecs(): IFrameCodecSet;
604
+ /**
605
+ * Pose le codec binaire du serveur — 1× au boot par `RealtimeService.init()`,
606
+ * depuis la clé `codec` de la configuration. Sans argument : texte seul.
607
+ *
608
+ * @param codec - le codec activé (`CBOR_FRAME_CODEC` de `nodefony/cbor`).
609
+ */
610
+ setFrameCodec(codec?: IFrameCodec): void;
611
+ /**
612
+ * Le codec des connexions texte — celui qu'emprunte aussi le REST du plan
613
+ * d'administration, pour qu'une valeur lue par `GET` soit celle que la socket
614
+ * livre (#618 D9). Avec un codec binaire activé, c'est le JSON à convention
615
+ * d'octets ; sinon le JSON natif.
616
+ */
617
+ get textFrameCodec(): IFrameCodec;
618
+ /**
619
+ * Pose les bornes des admissions (`authenticateTimeoutMs`, `allowTimeoutMs`, `maxPendingAllowsPerConnection`, `maxPendingAllowsTotal`) — 1× au boot par `RealtimeService.init()`.
620
+ */
621
+ setAdmissionLimits(limits: IAdmissionLimits): void;
364
622
  /**
365
623
  * Associe un token au peer (posé par `RealtimeController.onHandshake()` après
366
624
  * succès `authenticator.authenticate()` ou fallback anonyme). Lazy alloc de
@@ -377,6 +635,10 @@ export declare class RealtimeHub {
377
635
  * **Seam #1 (P13 → P6)** — pose le verrou de frame (cf {@link FrameAuthorizer}).
378
636
  * `null` retire la politique (bypass total). Posé 1× au boot par
379
637
  * `@nodefony/security` depuis les zones `realtime: true`. Cold path.
638
+ *
639
+ * @throws BootConfigurationError Le verrou ne déclare pas `judgesWrite`
640
+ * alors qu'une écriture distincte de la lecture est inscrite : il la
641
+ * laisserait ouverte.
380
642
  */
381
643
  setFrameAuthorizer(authorizer: FrameAuthorizer | null, options?: {
382
644
  readonly silentProbe?: FrameAuthorizer | null;
@@ -420,10 +682,7 @@ export declare class RealtimeHub {
420
682
  */
421
683
  hasUnenforcedChannelPolicies(): boolean;
422
684
  /**
423
- * **Seam #1b** — déclare la politique d'autorisation d'un canal/méthode
424
- * (`@RealtimeChannel`/`@RealtimeInbound` avec opts). Appelé au handshake par
425
- * le controller pour CHAQUE canal décoré (idempotent : même nom = écrase, les
426
- * controllers d'un même endpoint déclarent la même policy). Cold path.
685
+ * **Seam #1b** — pose la politique d'un canal/méthode en REMPLAÇANT ce que ce nom déclarait (voie programmatique ; les décorateurs passent par {@link RealtimeHub.declareChannelPolicy}). Cold path.
427
686
  *
428
687
  * Deux formes de nom, et la seconde garde une FAMILLE :
429
688
  *
@@ -433,15 +692,42 @@ export declare class RealtimeHub {
433
692
  * Le `*` remplace n'importe quelle suite de caractères ; tout le reste du
434
693
  * motif est littéral (échappé), y compris `.` et `(`.
435
694
  *
436
- * Précédence : le nom **exact** l'emporte toujours — c'est ainsi qu'on ouvre
437
- * une exception dans une famille gardée. Entre deux motifs, le plus
438
- * **spécifique** gagne (littéral le plus long).
695
+ * Précédence, SORTE PAR SORTE (ADR-0022 §6, décision 1 quater) : la lecture
696
+ * d'un nom vient de la déclaration la plus spécifique qui POSE une lecture —
697
+ * le nom **exact**, puis le motif au littéral le plus long ; l'écriture de
698
+ * même. C'est ainsi qu'on ouvre une exception dans une famille gardée, sans
699
+ * effacer la sorte que l'exception ne déclare pas.
439
700
  *
440
- * @param name - nom exact du canal (subscribe), de la méthode inbound, ou
701
+ * @param name - nom exact du canal, de la méthode entrante ou de l'action, ou
441
702
  * MOTIF de famille s'il contient `*`.
442
- * @param policy - exigences ({@link IChannelPolicy}) lues par security.
703
+ * @param policy - exigences de LECTURE ({@link IChannelPolicy}) lues par
704
+ * security ; seules exigées au `subscribe`.
705
+ * @param admission - instance de la classe d'admission quand `policy.allow`
706
+ * en est une ; ignorée sinon.
707
+ * @param write - exigences d'ÉCRITURE, exigées EN PLUS de la lecture sur une
708
+ * trame entrante ; `null` : ce nom n'en déclare pas.
709
+ * @throws BootConfigurationError Classe d'admission sans instance ; `allow`
710
+ * sur une écriture qui rencontre une lecture différente ; écriture distincte
711
+ * face à un verrou qui ne sait pas la juger.
712
+ */
713
+ registerChannelPolicy(name: string, policy: IChannelPolicy, admission?: IChannelAdmission, write?: IChannelPolicy | null): void;
714
+ /**
715
+ * Déclare la lecture et/ou l'écriture d'un nom en les AJOUTANT à ce qu'il déclarait — une sorte posée remplace la sienne, une sorte `null` garde celle d'avant. Voie des décorateurs (`channelPolicyRegistry.ts#registerControllerPolicies`). Cold path.
716
+ *
717
+ * @param name - nom exact ou motif.
718
+ * @param read - lecture déclarée, ou `null`.
719
+ * @param write - écriture déclarée, ou `null`.
720
+ * @param admission - instance de la classe d'admission désignée par l'une ou
721
+ * l'autre ; ignorée sinon.
722
+ * @throws BootConfigurationError Mêmes refus que {@link RealtimeHub.registerChannelPolicy}.
723
+ */
724
+ declareChannelPolicy(name: string, read: IChannelPolicy | null, write: IChannelPolicy | null, admission?: IChannelAdmission): void;
725
+ /**
726
+ * Pose le défaut FERMÉ d'une action (`{ authenticated: true }`) — son ÉCRITURE tant que rien d'exact ne déclare son nom et qu'aucun motif ne pose d'écriture ; une lecture de motif s'y ajoute sans l'ouvrir. Idempotent : une poignée de main n'en paie qu'une lecture.
727
+ *
728
+ * @param name - nom de l'action.
443
729
  */
444
- registerChannelPolicy(name: string, policy: IChannelPolicy): void;
730
+ declareActionDefault(name: string): void;
445
731
  /**
446
732
  * Nombre maximum de `*` dans un motif.
447
733
  *
@@ -459,6 +745,51 @@ export declare class RealtimeHub {
459
745
  * la frame `subscribe`/inbound. O(1), 0 alloc quand le registre est vide.
460
746
  */
461
747
  resolveChannelPolicy(name: string): IChannelPolicy | null;
748
+ /**
749
+ * Lecture ET écriture qui s'appliquent à ce nom, en UNE résolution — `null` si aucune politique ne le couvre (ADR-0022 §6, décision 1 quater).
750
+ *
751
+ * Même résolution que {@link resolveChannelPolicy} : l'entrée la plus
752
+ * spécifique (nom exact, puis motif) gouverne les deux sortes. Rendue telle
753
+ * quelle : aucune allocation, une seule recherche par trame jugée.
754
+ *
755
+ * @param name - nom du canal, de la méthode entrante ou de l'action.
756
+ */
757
+ resolveChannelPolicies(name: string): IChannelPolicies | null;
758
+ /**
759
+ * Retient la PREMIÈRE classe qui déclare une sorte de politique sous ce nom, et rend celle qui l'avait déjà déclarée — pour refuser au démarrage deux classes en désaccord.
760
+ *
761
+ * @param name - nom exact ou motif.
762
+ * @param kind - sorte déclarée : lecture ou écriture.
763
+ * @param owner - constructeur du contrôleur qui déclare.
764
+ * @param policy - la politique déclarée.
765
+ * @returns la déclaration antérieure de cette sorte, ou `null`.
766
+ */
767
+ claimChannelPolicy(name: string, kind: "read" | "write", owner: object, policy: IChannelPolicy): IChannelPolicyClaim | null;
768
+ /**
769
+ * Règle d'admission prête à appeler pour ce canal — `null` s'il ne déclare pas `allow`. Lue au `subscribe`, jamais par trame.
770
+ *
771
+ * Même résolution que {@link resolveChannelPolicy} (nom exact, puis motif le
772
+ * plus spécifique) : la règle suit TOUJOURS la politique qui s'applique.
773
+ */
774
+ resolveChannelAdmission(name: string): IChannelAdmissionRule | null;
775
+ /**
776
+ * Les politiques de cette classe de contrôleur sont-elles déjà inscrites ? Une poignée de main n'en paie que ce `has`.
777
+ *
778
+ * @param owner - le constructeur du contrôleur.
779
+ */
780
+ hasPolicyOwner(owner: object): boolean;
781
+ /**
782
+ * Marque les politiques de cette classe comme inscrites — APRÈS l'inscription réussie, jamais avant : une politique refusée doit l'être de nouveau à la poignée de main suivante, pas laisser le canal ouvert.
783
+ *
784
+ * @param owner - le constructeur du contrôleur.
785
+ */
786
+ addPolicyOwner(owner: object): void;
787
+ /**
788
+ * Ce nom EXACT porte-t-il une déclaration — motifs ignorés ?
789
+ *
790
+ * @param name - nom d'action ou de canal.
791
+ */
792
+ hasExactChannelPolicy(name: string): boolean;
462
793
  /** Des politiques de canal sont-elles déclarées (au moins une) ? */
463
794
  hasChannelPolicies(): boolean;
464
795
  /**
@@ -466,14 +797,14 @@ export declare class RealtimeHub {
466
797
  * DYNAMIQUE n'est couvert par aucune politique déclarée, alors que d'autres
467
798
  * canaux du module en portent une.
468
799
  *
469
- * Le registre des politiques est indexé par nom **EXACT** : `@RealtimeChannel`
470
- * déclare `"chat:room"`, mais un canal dérivé (`chat:room:1000` — cadence,
471
- * forage, suffixe) est servi par la fabrique dynamique et **ne matche pas**.
472
- * L'auteur croit alors son canal gardé ; il ne l'est pas, et rien ne le lui
473
- * dit. C'est ce silence qu'on ferme ici — et depuis qu'une politique peut se
474
- * déclarer par MOTIF (`chat:room:*`), cet avertissement ne parle plus que des
475
- * canaux qu'aucun motif ne couvre : il désigne un vrai trou, pas une limite
476
- * de l'outil.
800
+ * Le registre des politiques lit le nom **EXACT**, puis les **MOTIFS**
801
+ * déclarés ({@link resolveChannelPolicy}) : `@RealtimeChannel` déclare
802
+ * `"chat:room"`, mais un canal dérivé (`chat:room:1000` — cadence, forage,
803
+ * suffixe) servi par la fabrique dynamique ne matche ce nom que si un motif
804
+ * (`chat:room:*`) le couvre. Sans motif, l'auteur croit son canal gardé ; il
805
+ * ne l'est pas, et rien ne le lui dit. C'est ce silence qu'on ferme ici :
806
+ * l'avertissement ne parle que des canaux qu'aucun motif ne couvre — un vrai
807
+ * trou, pas une limite de l'outil.
477
808
  *
478
809
  * Appelé par le contrôleur, seul à savoir par quelle voie le canal a été
479
810
  * servi. Muet si aucune politique n'est déclarée (module aux canaux libres :
@@ -495,25 +826,53 @@ export declare class RealtimeHub {
495
826
  * @param factory - {@link ChannelFactory} qui crée le provider partagé.
496
827
  */
497
828
  registerSystemChannel(channel: string, factory: ChannelFactory): void;
829
+ /**
830
+ * Ouvre (ou referme) la réception des journaux du navigateur, avec ses bornes.
831
+ *
832
+ * @param limits - bornes de débit et de taille, ou `null` pour refermer le canal.
833
+ */
834
+ setClientLogsLimits(limits: IClientLogsLimits | null): void;
835
+ /** Bornes en vigueur, ou `null` si le canal montant est fermé. */
836
+ get clientLogsLimits(): IClientLogsLimits | null;
837
+ /**
838
+ * La raison d'un refus du verrou, quand il sait la dire — `null` sinon, ou si l'explication lève.
839
+ *
840
+ * Chemin FROID : appelé après un refus, jamais par trame acceptée. Le
841
+ * verdict, lui, est déjà rendu : une explication fautive ne change rien à la
842
+ * décision, elle se tait.
843
+ *
844
+ * @param frame - la trame refusée.
845
+ * @param peer - la connexion qui l'a émise.
846
+ * @returns la phrase, ou `null`.
847
+ */
848
+ explainDenial(frame: unknown, peer: JsonRpcPeer): string | null;
498
849
  /**
499
850
  * Pont hot-path entre `JsonRpcPeer.beforeDispatch` et le verrou métier : lit
500
851
  * le token déjà caché du peer (O(1), 0 lecture base) puis délègue à
501
- * l'authorizer. `true` (autorisée) si aucun verrou posé (bypass 0-coût) —
502
- * mais en pratique `beforeDispatch` n'est branché que si `hasFrameAuthorizer()`.
852
+ * l'authorizer. `true` (autorisée) si aucun verrou posé : `beforeDispatch`
853
+ * est armé sur TOUTE connexion, et c'est ici que le bypass se paie — un
854
+ * appel et une comparaison de champ.
503
855
  *
504
856
  * @param frame - frame JSON-RPC entrante (brute) à autoriser.
505
857
  * @param peer - peer émetteur (clé du mapping `peer → token`).
506
858
  * @returns `true` si la frame peut être dispatchée, `false` si refusée.
507
859
  */
860
+ runAuthorizer(frame: unknown, peer: JsonRpcPeer): boolean;
508
861
  /**
509
- * Ouvre (ou referme) la réception des journaux du navigateur, avec ses bornes.
862
+ * Inscrit une connexion qui vient de retenir son premier « oui » du verrou.
510
863
  *
511
- * @param limits - bornes de débit et de taille, ou `null` pour refermer le canal.
864
+ * Appelé par `gateFrame`, jamais par trame chaude ; à équilibrer par
865
+ * {@link untrackRetainedVerdicts} à la fermeture.
866
+ *
867
+ * @param state - état de la connexion.
512
868
  */
513
- setClientLogsLimits(limits: IClientLogsLimits | null): void;
514
- /** Bornes en vigueur, ou `null` si le canal montant est fermé. */
515
- get clientLogsLimits(): IClientLogsLimits | null;
516
- runAuthorizer(frame: unknown, peer: JsonRpcPeer): boolean;
869
+ trackRetainedVerdicts(state: IRealtimeConnectionState): void;
870
+ /**
871
+ * Retire une connexion fermée du registre des verdicts retenus.
872
+ *
873
+ * @param state - état de la connexion.
874
+ */
875
+ untrackRetainedVerdicts(state: IRealtimeConnectionState): void;
517
876
  /**
518
877
  * Ce peer pourrait-il s'abonner à ce canal ? — même décision que `subscribe`,
519
878
  * sans trace ni effet de bord.
@@ -533,6 +892,53 @@ export declare class RealtimeHub {
533
892
  /** Dispose tous les providers et vide le hub (arrêt process / reset de test). */
534
893
  clear(): void;
535
894
  }
536
- /** Renvoie le hub realtime du process (singleton lazy). */
895
+ /**
896
+ * Le hub lu à travers le contrat `C` : mêmes méthodes que {@link RealtimeHub},
897
+ * mais `publish` n'accepte que les canaux de `serverToClient`, chacun avec la
898
+ * charge qu'il déclare. C'est le MÊME objet que le hub libre — une vue de type,
899
+ * sans un octet ni une instruction de plus.
900
+ *
901
+ * @typeParam C - le contrat de l'application.
902
+ */
903
+ export interface IContractRealtimeHub<C extends IRealtimeContract> extends Omit<RealtimeHub, "publish" | "publishToUser"> {
904
+ /**
905
+ * Livre à UNE identité la charge que le contrat déclare pour le canal
906
+ * personnel (clé gabarit `` [k: `user:${string}`]: … `` de `serverToClient`)
907
+ * — cf {@link RealtimeHub.publishToUser}. Contrat sans cette clé : ne compile
908
+ * pas, la forme du message privé se déclare d'abord.
909
+ *
910
+ * @param userId - l'`id` STABLE du compte, jamais le login.
911
+ */
912
+ publishToUser(userId: string, payload: UserChannelPayload<ContractServerEmit<C>> & object): void;
913
+ /**
914
+ * Publie sur un canal déclaré par le contrat (fan-out local, puis backplane
915
+ * si le canal est broadcast) — cf {@link RealtimeHub.publish}.
916
+ *
917
+ * @throws TypeError si la charge n'est pas un objet ({@link isRealtimePayload}).
918
+ */
919
+ publish<K extends EventNames<ContractServerEmit<C>>>(channel: K, payload: EventPayload<ContractServerEmit<C>, K> & object): void;
920
+ }
921
+ /**
922
+ * Renvoie le hub realtime du process (singleton lazy) — forme LIBRE : tout nom
923
+ * de canal, toute charge objet.
924
+ */
537
925
  export declare function getRealtimeHub(): RealtimeHub;
926
+ /**
927
+ * Renvoie le hub realtime du process lu à travers le contrat `C` : la diffusion
928
+ * d'un service, d'un job ou d'une boucle de jeu est vérifiée par le contrat
929
+ * partagé avec le navigateur. Même objet que la forme libre : 0 octet, 0 ns.
930
+ *
931
+ * Le contrat est choisi par l'appelant — rien n'empêche d'en nommer un autre que
932
+ * celui du contrôleur qui sert le canal ; c'est le fichier partagé qui fait foi.
933
+ *
934
+ * @typeParam C - le contrat de l'application.
935
+ *
936
+ * @example
937
+ * ```ts
938
+ * const chat = getRealtimeHub<ChatContract>();
939
+ * chat.publish("chat:room", { from: "ana", text: "salut" }); // charge vérifiée
940
+ * getRealtimeHub().publish("n'importe:quoi", { x: 1 }); // forme libre
941
+ * ```
942
+ */
943
+ export declare function getRealtimeHub<C extends IRealtimeContract>(): IContractRealtimeHub<C>;
538
944
  export default RealtimeHub;
@@ -31,7 +31,7 @@ export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEvents
31
31
  private readonly hub;
32
32
  constructor(hub?: import("./RealtimeHub.js").RealtimeHub);
33
33
  /** Émet sur un canal — fan-out aux abonnés via le hub (+ backplane si broadcast). */
34
- publish<K extends string>(channel: K, payload?: K extends EventNames<Emit> ? EventPayload<Emit, K> : unknown): void;
34
+ publish<K extends string>(channel: K, payload?: K extends EventNames<Emit> ? EventPayload<Emit, K> : object): void;
35
35
  /**
36
36
  * S'abonne (ref-compté) à un canal pour le RECEVOIR. Au 1ᵉʳ consommateur, pose un
37
37
  * sink d'**écoute passive** auprès du hub : le service reçoit ce qui passe, sans