@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
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Ordres de contrôle entre serveurs — la LISTE FERMÉE de ce qui circule sur le
3
+ * canal `PLATFORM_BUS_CHANNELS.control` (ADR-0022 §6 décision 4).
4
+ *
5
+ * Un ordre ne fait qu'ENLEVER : terminer un canal, éjecter un compte d'un
6
+ * canal. Aucun n'admet — seul `allow` fait entrer. Il voyage dans la charge
7
+ * d'un message ordinaire du bus, donc sous le sceau de l'enveloppe
8
+ * (`envelope.ts#sealBackplaneEnvelope`), sans champ neuf dans l'enveloppe ni
9
+ * méthode neuve dans `IBackplane` : un driver tiers le transporte sans le savoir.
10
+ */
11
+ /** Longueur maximale d'un champ texte d'un ordre (nom de canal, identifiant de compte). */
12
+ export declare const MAX_CONTROL_FIELD_LENGTH = 1024;
13
+ /** Terminer un canal sur chaque serveur : ses membres reçoivent `realtime:closed { reason: "ended" }`. */
14
+ export interface ICloseChannelOrder {
15
+ /** Discriminant de l'ordre. */
16
+ readonly op: "close";
17
+ /** Nom exact du canal. */
18
+ readonly channel: string;
19
+ }
20
+ /** Éjecter d'un canal, sur chaque serveur, toutes les connexions d'un compte. */
21
+ export interface IEvictUserOrder {
22
+ /** Discriminant de l'ordre. */
23
+ readonly op: "evictUser";
24
+ /** Nom exact du canal. */
25
+ readonly channel: string;
26
+ /** Identifiant du compte (`IRealtimeConnection.userId`). */
27
+ readonly user: string;
28
+ /**
29
+ * Génération d'appartenance visée — RÉSERVÉE : aucun exemplaire 10.0 ne
30
+ * l'émet, et un exemplaire qui ne sait pas s'en servir l'ignore et éjecte
31
+ * toutes les générations (plus large, jamais moins). Elle portera en 10.x la
32
+ * parade au rejeu d'une éjection sur un canal durable (ADR-0022 §6 d4, S-rt-48).
33
+ */
34
+ readonly generation?: number;
35
+ }
36
+ /** Un ordre de contrôle reconnu. */
37
+ export type ControlOrder = ICloseChannelOrder | IEvictUserOrder;
38
+ /**
39
+ * Un texte venu du bus est-il recevable — chaîne non vide d'au plus {@link MAX_CONTROL_FIELD_LENGTH} caractères ?
40
+ *
41
+ * Seule borne des textes du bus (nom de canal, identifiant de compte), lue par
42
+ * les ordres de contrôle ET par les messages de présence (ADR-0025 P2).
43
+ *
44
+ * @param value - champ reçu (forme inconnue).
45
+ */
46
+ export declare function isBoundedBusText(value: unknown): value is string;
47
+ /**
48
+ * La charge est-elle un ordre d'un TYPE que cette version ne connaît pas — `op` lisible, mais hors de la liste fermée ?
49
+ *
50
+ * C'est ce qu'émet un serveur d'une version plus récente pendant un
51
+ * déploiement progressif (ADR-0025, « Règle de lecture du bus ») : l'ordre
52
+ * n'est pas appliqué, mais il ne se compte pas comme une injection. Rien
53
+ * d'autre que `op` n'est lu — ses autres champs appartiennent à une version
54
+ * qui n'est pas celle-ci. Une charge sans `op` textuel, ou d'un type CONNU de
55
+ * forme fausse, n'est pas « inconnue » : elle est invalide.
56
+ *
57
+ * @param payload - la charge du message reçu (forme inconnue).
58
+ * @returns `true` si le type d'ordre est inconnu de cette version.
59
+ */
60
+ export declare function isUnknownControlOrder(payload: unknown): boolean;
61
+ /**
62
+ * Lit un ordre de contrôle dans une charge venue du bus — `null` si la forme n'est pas celle d'un ordre connu.
63
+ *
64
+ * Une seule garde pour les deux bords : l'émetteur la passe avant de publier
65
+ * (il n'émet jamais ce que ses pairs refuseraient), le récepteur avant
66
+ * d'appliquer. Lecture par champs PROPRES (`ownField`) : une charge dont le
67
+ * prototype porterait `op` ne passe pas. Un champ inconnu est toléré — c'est
68
+ * ce qui laisse une version suivante ajouter un champ sans faire refuser ses
69
+ * ordres pendant un déploiement progressif ; un champ CONNU de forme fausse
70
+ * fait refuser l'ordre entier.
71
+ *
72
+ * @param payload - la charge du message reçu (forme inconnue).
73
+ * @returns l'ordre, ou `null`.
74
+ */
75
+ export declare function readControlOrder(payload: unknown): ControlOrder | null;
@@ -1,3 +1,4 @@
1
+ import { type IFrameCodec } from "nodefony";
1
2
  import type { IBackplaneMessage } from "../../interfaces/IBackplane.js";
2
3
  /**
3
4
  * Sérialise un message backplane, scellé si un secret est fourni.
@@ -19,9 +20,85 @@ export declare function sealBackplaneEnvelope(msg: IBackplaneMessage, secret?: s
19
20
  * `sig`. Sans secret, l'enveloppe nue est acceptée (compat ; l'alerte de boot qui
20
21
  * signale ce mode non authentifié est portée par le wiring du driver).
21
22
  *
23
+ * Le sceau se vérifie sur la tranche REÇUE de la charge quand l'enveloppe a la
24
+ * forme qu'écrit {@link sealBackplaneEnvelope} — aucune re-sérialisation ; une
25
+ * autre forme retombe sur la re-sérialisation de la charge lue.
26
+ *
22
27
  * @param raw - message brut lu sur le bus.
23
28
  * @param secret - secret partagé attendu, ou `null`/`undefined` si aucun.
24
29
  * @returns le message vérifié, ou `null` s'il doit être ignoré (malformé, non
25
30
  * scellé alors qu'un secret est exigé, sceau invalide).
26
31
  */
27
32
  export declare function openBackplaneEnvelope(raw: string, secret?: string | null): IBackplaneMessage | null;
33
+ /**
34
+ * Encode un message backplane pour le **sujet binaire** du bus (ADR-0021 S6) :
35
+ * la MÊME enveloppe `{ channel, payload, originId }`, encodée par le codec
36
+ * binaire du serveur, octets natifs dans `payload`, scellée si un secret est
37
+ * fourni.
38
+ *
39
+ * Scellée, la charge est encodée UNE fois : le sceau se calcule sur ses octets,
40
+ * et l'enveloppe s'assemble autour d'eux. Un codec qui n'écrit pas le CBOR le plus
41
+ * court (constaté, cf {@link framesAsCbor}) encode l'enveloppe entière une seconde
42
+ * fois — correct pour tout format, plus coûteux.
43
+ *
44
+ * @param msg - message à transporter (canal, charge, origine).
45
+ * @param codec - le codec binaire du serveur (`CBOR_FRAME_CODEC` de `nodefony/cbor`).
46
+ * @param secret - secret partagé entre pods ; `null`/`undefined` → enveloppe nue.
47
+ * @returns les octets à publier sur le sujet binaire.
48
+ * @throws TypeError si le codec rend du texte.
49
+ */
50
+ export declare function sealBackplaneEnvelopeBytes(msg: IBackplaneMessage, codec: IFrameCodec, secret?: string | null): Uint8Array;
51
+ /**
52
+ * Décode et **vérifie** une enveloppe reçue sur le sujet binaire du bus — même
53
+ * règle que {@link openBackplaneEnvelope} : fail-closed strict sous un secret,
54
+ * aucun downgrade en retirant `sig`.
55
+ *
56
+ * Le sceau se vérifie sur la tranche REÇUE de la charge quand l'enveloppe a la
57
+ * forme qu'assemble l'émetteur — aucun ré-encodage ; une autre forme retombe sur
58
+ * le ré-encodage de la charge décodée (le codec CBOR du cœur rend la forme la plus
59
+ * courte de chaque valeur, donc émetteur et récepteur signent les mêmes octets).
60
+ *
61
+ * @param raw - octets lus sur le sujet binaire.
62
+ * @param codec - le codec binaire du serveur.
63
+ * @param secret - secret partagé attendu, ou `null`/`undefined` si aucun.
64
+ * @returns le message vérifié, ou `null` s'il doit être ignoré.
65
+ */
66
+ export declare function openBackplaneEnvelopeBytes(raw: Uint8Array, codec: IFrameCodec, secret?: string | null): IBackplaneMessage | null;
67
+ /**
68
+ * Reconnaît l'**écho** d'un pod — ce que le bus lui renvoie de ses propres
69
+ * publications — à la FIN du message, sans le décoder (ADR-0021 S6, ADR-0025).
70
+ *
71
+ * Un bus pub/sub comme Redis rend à l'émetteur chacune de ses publications : sur un
72
+ * pod chargé, c'est un message reçu sur deux, et la présence répartie en fait un
73
+ * signal (le bus est vivant tant que l'écho revient). Le reconnaître coûte, sous un
74
+ * secret, UN HMAC sur la tranche de la charge : ni `JSON.parse`, ni décodage CBOR.
75
+ *
76
+ * Sous un secret, l'écho est VÉRIFIÉ avant d'être compté : un tiers qui forgerait
77
+ * l'origine de ce pod pour simuler un bus vivant échoue au sceau. Sans secret, rien
78
+ * ne peut l'être — le bus est ouvert, et le wiring l'annonce au démarrage.
79
+ *
80
+ * Un message que ce lecteur ne reconnaît pas (forme tierce, sceau faux) n'est PAS
81
+ * jeté : il suit la voie ordinaire, qui le vérifie en entier.
82
+ */
83
+ export declare class BackplaneEcho {
84
+ #private;
85
+ /**
86
+ * @param originId - identité de CE pod sur le bus.
87
+ * @param secret - secret partagé, ou `null` sur un bus non scellé.
88
+ */
89
+ constructor(originId: string, secret: string | null);
90
+ /**
91
+ * Ce message texte est-il l'écho de ce pod (sceau vérifié sous un secret) ?
92
+ *
93
+ * @param raw - message brut lu sur le sujet texte.
94
+ */
95
+ text(raw: string): boolean;
96
+ /**
97
+ * Ce message binaire est-il l'écho de ce pod (sceau vérifié sous un secret) ?
98
+ *
99
+ * @param raw - octets lus sur le sujet binaire.
100
+ * @param codec - le codec binaire du serveur ; un codec qui n'écrit pas le CBOR
101
+ * attendu (cf {@link framesAsCbor}) laisse tout message à la voie ordinaire.
102
+ */
103
+ bytes(raw: Uint8Array, codec: IFrameCodec): boolean;
104
+ }
@@ -0,0 +1,48 @@
1
+ import type { IFrameCodec } from "nodefony";
2
+ import type { IBackplaneMessage } from "../../interfaces/IBackplane.js";
3
+ import type { BackplaneNotice } from "./publishQueue.js";
4
+ /**
5
+ * **Entrée d'un bus partagé** — le tri de ce qu'un pilote reçoit, mutualisé pour
6
+ * que tout pilote à transport réseau (Redis aujourd'hui ; Kafka, NATS, RabbitMQ…
7
+ * demain) l'hérite sans le réécrire : écho de ce pod reconnu et compté, sceau
8
+ * vérifié, refus comptés et annoncés.
9
+ *
10
+ * Le pilote ne garde que son transport : il passe chaque message reçu à
11
+ * {@link text} ou {@link bytes} et remet au hub ce qui en sort.
12
+ *
13
+ * Ordre du tri, du moins cher au plus cher :
14
+ * 1. l'ÉCHO de ce pod se reconnaît à la fin du message, sans le décoder — un HMAC
15
+ * sous un secret (cf {@link BackplaneEcho}) ;
16
+ * 2. sinon, l'enveloppe est ouverte et son sceau vérifié sur la tranche reçue ;
17
+ * 3. un message ouvert qui porte l'origine de ce pod sous une autre forme est
18
+ * encore un écho — compté, jamais réinjecté (anti-boucle).
19
+ */
20
+ export declare class BackplaneIngress {
21
+ #private;
22
+ /**
23
+ * @param originId - identité de CE pod sur le bus.
24
+ * @param secret - secret partagé, ou `null` sur un bus non scellé.
25
+ * @param notice - annonce des sceaux refusés ; omis = silencieux (tests).
26
+ */
27
+ constructor(originId: string, secret: string | null, notice?: BackplaneNotice | null);
28
+ /**
29
+ * Trie un message reçu sur le sujet TEXTE du bus.
30
+ *
31
+ * @param raw - message brut tel que lu sur le transport.
32
+ * @returns le message à remettre au hub, ou `null` s'il est jeté (écho de ce
33
+ * pod, malformé, sceau refusé — chacun compté).
34
+ */
35
+ text(raw: string): IBackplaneMessage | null;
36
+ /**
37
+ * Trie un message reçu sur le sujet BINAIRE du bus (ADR-0021 S6).
38
+ *
39
+ * @param raw - octets tels que lus sur le transport.
40
+ * @param codec - le codec binaire du serveur.
41
+ * @returns le message à remettre au hub, ou `null` s'il est jeté.
42
+ */
43
+ bytes(raw: Uint8Array, codec: IFrameCodec): IBackplaneMessage | null;
44
+ /** Échos de ce pod reçus du bus (monotone) — le bus rend ce qu'on y publie. */
45
+ get echoTotal(): number;
46
+ /** Messages jetés au sceau refusé, sous un secret (monotone). */
47
+ get sealRejectedTotal(): number;
48
+ }
@@ -0,0 +1,150 @@
1
+ import type { PresencePairs } from "nodefony";
2
+ /**
3
+ * Messages de la présence répartie entre serveurs — la LISTE FERMÉE de ce qui
4
+ * circule sur `PLATFORM_BUS_CHANNELS.presence` (lots, demandes, réponses d'état)
5
+ * et `PLATFORM_BUS_CHANNELS.presenceAlive` (preuve de vie), ADR-0025 P2.
6
+ *
7
+ * Comme un ordre de contrôle (`controlOrder.ts#readControlOrder`), chaque
8
+ * message voyage dans la charge d'un message ordinaire du bus, sous le sceau de
9
+ * l'enveloppe : aucun champ neuf dans l'enveloppe, aucune méthode neuve dans
10
+ * `IBackplane`. L'émetteur (`originId`) se lit dans l'enveloppe, jamais dans la
11
+ * charge.
12
+ *
13
+ * Lecture par champs PROPRES (`ownField`), textes bornés (`isBoundedBusText`),
14
+ * entiers sûrs ; un champ inconnu est toléré (déploiement progressif), un champ
15
+ * connu de forme fausse fait refuser le message entier. Les tableaux rendus
16
+ * sont CEUX reçus, validés en place — aucune copie, aucune allocation par
17
+ * membre : le lecteur ne les modifie pas, l'appelant non plus.
18
+ */
19
+ /** Plus petit intervalle de preuve de vie qu'un pair peut annoncer (ms). */
20
+ export declare const MIN_PRESENCE_INTERVAL_MS = 100;
21
+ /** Plus grand intervalle de preuve de vie qu'un pair peut annoncer (ms) — 10 min. */
22
+ export declare const MAX_PRESENCE_INTERVAL_MS = 600000;
23
+ /**
24
+ * Plafond d'identités nommées qu'un pod peut annoncer pour UN canal — au-delà,
25
+ * l'entrée est refusée (ADR-0025 P2, « plafond de membres par pod et par canal »).
26
+ */
27
+ export declare const MAX_PRESENCE_MEMBERS = 100000;
28
+ /** Plafond d'entrées de canal dans un même message (lot, demande ou part de réponse). */
29
+ export declare const MAX_PRESENCE_CHANNELS = 4096;
30
+ /** Plafond de parts d'une réponse d'état (`n`). */
31
+ export declare const MAX_PRESENCE_PARTS = 4096;
32
+ /**
33
+ * Changements d'un pod sur UN canal, en valeurs ABSOLUES : rejouer une entrée
34
+ * déjà comprise dans un état ne change rien.
35
+ */
36
+ export interface IPresenceChannelDelta {
37
+ /** Nom exact du canal. */
38
+ readonly channel: string;
39
+ /** Identités dont le NOMBRE d'onglets sur ce pod change, à plat `[identité, onglets, …]` (onglets ≥ 1) ; `null` si aucune. */
40
+ readonly set: PresencePairs | null;
41
+ /** Identités qui n'ont plus aucun onglet sur ce pod ; `null` si aucune. */
42
+ readonly gone: readonly string[] | null;
43
+ /** Connexions non nommées de ce pod sur ce canal — valeur absolue. */
44
+ readonly unnamed: number;
45
+ }
46
+ /** État COMPLET d'un pod sur un canal, dans une réponse d'état. */
47
+ export interface IPresenceChannelState {
48
+ /** Nom exact du canal. */
49
+ readonly channel: string;
50
+ /** Toutes les identités nommées de ce pod, à plat `[identité, onglets, …]` (onglets ≥ 1). */
51
+ readonly members: PresencePairs;
52
+ /** Connexions non nommées de ce pod sur ce canal. */
53
+ readonly unnamed: number;
54
+ }
55
+ /** Lot de changements d'un pod, scellé toutes les 50 ms, chaîné par `prev`. */
56
+ export interface IPresenceBatch {
57
+ /** Discriminant. */
58
+ readonly op: "batch";
59
+ /** Vie du processus émetteur (`Date.now() × 1024 + aléa sur 10 bits`). */
60
+ readonly incarnation: number;
61
+ /** Numéro du message, strictement croissant par (`originId`, incarnation), ≥ 1. */
62
+ readonly seq: number;
63
+ /** `seq` du lot précédent de l'émetteur, `0` pour le premier ; `< seq`. */
64
+ readonly prev: number;
65
+ /** Changements par canal. */
66
+ readonly channels: readonly IPresenceChannelDelta[];
67
+ }
68
+ /** Demande d'état — le *fetch* de RFC 2778 §2.1, la *presence probe* de RFC 6121 §4.3. */
69
+ export interface IPresenceRequest {
70
+ /** Discriminant. */
71
+ readonly op: "request";
72
+ /** Vie du processus demandeur. */
73
+ readonly incarnation: number;
74
+ /** Numéro du message (même suite que les lots du demandeur). */
75
+ readonly seq: number;
76
+ /** Canaux dont l'état est demandé. */
77
+ readonly channels: readonly string[];
78
+ /**
79
+ * Pod visé (`originId`) — il répond TOUJOURS, même vide (RFC 6121 §4.3.2) ;
80
+ * `null` = demande non ciblée, servie par les seuls pods qui ont des présents.
81
+ */
82
+ readonly to: string | null;
83
+ }
84
+ /**
85
+ * Une part d'une réponse d'état. Les `n` parts d'une réponse partent d'un bloc,
86
+ * à `seq` consécutifs (`seq` de la part `i` = `seq` de la part 0 + `i`), et
87
+ * s'appliquent d'un bloc : une part perdue laisse la vue inchangée.
88
+ */
89
+ export interface IPresenceStatePart {
90
+ /** Discriminant. */
91
+ readonly op: "state";
92
+ /** Vie du processus qui répond. */
93
+ readonly incarnation: number;
94
+ /** Numéro du message. */
95
+ readonly seq: number;
96
+ /** `seq` du dernier lot émis avant la réponse — il ferme le trou (RFC 5263 §4.5) ; `< seq`. */
97
+ readonly last: number;
98
+ /** Rang de la part, `0 ≤ i < n`. */
99
+ readonly i: number;
100
+ /** Nombre de parts de la réponse, `1 ≤ n ≤ MAX_PRESENCE_PARTS`. */
101
+ readonly n: number;
102
+ /** États complets par canal (un canal peut s'étendre sur plusieurs parts). */
103
+ readonly channels: readonly IPresenceChannelState[];
104
+ }
105
+ /** Message lu sur `PLATFORM_BUS_CHANNELS.presence`. */
106
+ export type PresenceMessage = IPresenceBatch | IPresenceRequest | IPresenceStatePart;
107
+ /** Preuve de vie, lue sur `PLATFORM_BUS_CHANNELS.presenceAlive` — quelques dizaines d'octets, indépendante du nombre de canaux. */
108
+ export interface IPresenceAlive {
109
+ /** Vie du processus émetteur. */
110
+ readonly incarnation: number;
111
+ /** Numéro du message (même suite que les lots). */
112
+ readonly seq: number;
113
+ /** `seq` du dernier lot émis, `0` si aucun ; `< seq`. */
114
+ readonly last: number;
115
+ /** Intervalle de l'émetteur (ms), ANNONCÉ : il fixe le délai d'oubli chez les pairs (RFC 6665 §3.1.1). */
116
+ readonly interval: number;
117
+ /** `true` = arrêt propre : les pairs l'oublient aussitôt (RFC 6121 §4.5.1). */
118
+ readonly final: boolean;
119
+ }
120
+ /**
121
+ * Lit un message du canal de présence — `null` si sa forme n'est pas celle d'un message connu.
122
+ *
123
+ * Une seule garde pour les deux bords : l'émetteur la passe en test (il n'émet
124
+ * jamais ce que ses pairs refuseraient), le récepteur avant d'appliquer.
125
+ *
126
+ * @param payload - la charge du message reçu (forme inconnue).
127
+ * @returns le message, ou `null` (forme invalide, ou type inconnu : cf
128
+ * {@link isUnknownPresenceMessage} pour les distinguer).
129
+ */
130
+ export declare function readPresenceMessage(payload: unknown): PresenceMessage | null;
131
+ /**
132
+ * La charge est-elle un message de présence d'un TYPE que cette version ne connaît pas — `op` lisible, hors de la liste fermée ?
133
+ *
134
+ * C'est ce qu'émet un serveur plus récent pendant un déploiement progressif :
135
+ * jeté et compté dans `ingressUnknownDiscardedTotal`, jamais comme une
136
+ * injection (ADR-0025, « Règle de lecture du bus »).
137
+ *
138
+ * @param payload - la charge du message reçu (forme inconnue).
139
+ */
140
+ export declare function isUnknownPresenceMessage(payload: unknown): boolean;
141
+ /**
142
+ * Lit une preuve de vie — `null` si sa forme est fausse.
143
+ *
144
+ * `interval` hors de [{@link MIN_PRESENCE_INTERVAL_MS}, {@link MAX_PRESENCE_INTERVAL_MS}]
145
+ * est refusé : un pair qui annoncerait 1 ms se ferait oublier à chaque tic, un
146
+ * qui annoncerait une heure garderait ses fantômes une demi-journée.
147
+ *
148
+ * @param payload - la charge du message reçu (forme inconnue).
149
+ */
150
+ export declare function readPresenceAlive(payload: unknown): IPresenceAlive | null;
@@ -24,6 +24,18 @@
24
24
  * Ce qui est jeté est le message **nouveau**, pas le plus ancien : contrairement au
25
25
  * WS (drop latest-wins sur une file qu'on possède), ce qui est déjà remis au client
26
26
  * réseau ne peut plus être repris.
27
+ *
28
+ * **Les ordres de contrôle ne sont JAMAIS jetés** (#762) : terminer un canal ou
29
+ * éjecter un compte passe hors quota, même file saturée. Sinon, sous charge, un
30
+ * modérateur éjecte un compte sur un serveur et ce compte continue de lire sur un
31
+ * autre, sans que personne le sache. Les ordres sont rares et petits (une décision
32
+ * du serveur, quelques dizaines d'octets) : les exempter ne rouvre pas la mémoire.
33
+ * La **preuve de vie** de la présence répartie passe aussi hors quota (ADR-0025
34
+ * P2) : jetée, elle ferait oublier un pod vivant chez ses pairs ; elle est petite
35
+ * et d'intervalle borné. Liste des canaux prioritaires : `isPriorityBusChannel`
36
+ * (cœur), seule source.
37
+ * Un ordre que le bus REFUSE malgré tout (bus injoignable) est compté à part
38
+ * (`controlLostTotal`) et annoncé une fois par épisode.
27
39
  */
28
40
  import type { IBackplaneQueueInfo } from "../../interfaces/IBackplane.js";
29
41
  /** Sévérité d'une transition de la file — mappée telle quelle sur le syslog. */
@@ -59,12 +71,24 @@ export declare class BackplanePublishQueue {
59
71
  * (sinon elle ne partirait jamais — famine silencieuse), quitte à dépasser
60
72
  * transitoirement. Même règle que le drop WS, qui teste `bufferedAmount` seul.
61
73
  *
74
+ * Un canal prioritaire (`isPriorityBusChannel` du cœur : ordre de contrôle,
75
+ * preuve de vie de la présence) est admis hors quota : la file l'applique, pas
76
+ * le driver, pour qu'un driver qui emploie la file la porte sans y penser.
77
+ *
78
+ * @param channel - canal logique de la publication.
62
79
  * @param bytes - taille de la charge sérialisée.
63
80
  * @param emit - envoi réel ; appelé UNIQUEMENT si la publication est admise.
64
81
  * Son retour est suivi s'il s'agit d'une promesse (transport asynchrone).
65
82
  * @returns `true` si la publication est partie, `false` si elle a été jetée.
66
83
  */
67
- send(bytes: number, emit: () => unknown): boolean;
84
+ send(channel: string, bytes: number, emit: () => unknown): boolean;
85
+ /**
86
+ * Attend que toutes les publications en vol soient acquittées ou refusées par le bus — à l'arrêt, pour que ce qui est déjà émis (la preuve de vie finale de la présence) parte avant la sortie du processus.
87
+ *
88
+ * Ne borne rien : un bus qui ne répond plus ne la résout jamais. L'appelant
89
+ * pose la limite (l'arrêt du noyau a la sienne).
90
+ */
91
+ settled(): Promise<void>;
68
92
  /** Compteurs pour la carte d'identité du backplane (sonde + Studio). */
69
93
  describe(): IBackplaneQueueInfo;
70
94
  }
@@ -1,4 +1,14 @@
1
+ import type { ISubscriberIdentity } from "nodefony";
1
2
  import type { IRealtimeToken } from "../../interfaces/IRealtimeToken.js";
3
+ /**
4
+ * Identité d'admission de l'ANONYME — gelée et partagée par toutes les connexions non authentifiées (0 allocation par connexion).
5
+ *
6
+ * Porte l'invariant de l'ADR-0021 §S10 : anonyme ⇔ `authenticated === false`,
7
+ * `id === null` et `issuer === null`. Un canal qui déclare `allow` refuse
8
+ * cette identité d'office, sauf option explicite `anonymous: true` — ce qui
9
+ * garantit que `user:${who.id}` ne vaut jamais `"user:null"`.
10
+ */
11
+ export declare const ANONYMOUS_SUBSCRIBER_IDENTITY: ISubscriberIdentity;
2
12
  /**
3
13
  * Token realtime ANONYME — fallback Zero Trust quand aucun matcher
4
14
  * `IRealtimeAuthenticatorMatcher` ne capture le handshake.
@@ -0,0 +1,138 @@
1
+ import type { ContractActionNames, ContractClientEmit, ContractServerActions, ContractServerEmit, ContractServerListen, ContractServerMethods, EventNames, IRealtimeContract, TypedRpcActionHandler } from "nodefony";
2
+ import { RealtimeController } from "./RealtimeController.js";
3
+ /**
4
+ * Clé de l'étiquette de type qui relie une classe à son contrat. Déclaration
5
+ * ambiante : aucun symbole n'existe à l'exécution, seul le compilateur la voit.
6
+ */
7
+ declare const contractType: unique symbol;
8
+ /**
9
+ * Table COMPLÈTE des méthodes que le serveur doit au contrat : une entrée par nom
10
+ * de `serverMethods`, chacune recevant ses paramètres lus par `Jsonify` et rendant
11
+ * le résultat déclaré. Une méthode oubliée, un nom hors contrat ou une forme
12
+ * fausse ne compilent pas.
13
+ */
14
+ export type ContractRealtimeMethodHandlers<C extends IRealtimeContract> = {
15
+ [K in ContractActionNames<ContractServerMethods<C>>]: TypedRpcActionHandler<ContractServerMethods<C>, K>;
16
+ };
17
+ /**
18
+ * Noms qu'un contrat déclare et qu'une politique peut garder : ses méthodes
19
+ * serveur et les canaux de ses deux directions. Un canal déclaré par gabarit
20
+ * (`user:${string}`) admet aussi son motif (`user:*`). Direction non déclarée :
21
+ * tout nom.
22
+ */
23
+ export type ContractPolicyNames<C extends IRealtimeContract> = ContractActionNames<ContractServerMethods<C>> | EventNames<ContractServerEmit<C>> | EventNames<ContractClientEmit<C>>;
24
+ /**
25
+ * Le contrat qu'une classe de contrôleur implémente, lu sur son étiquette de
26
+ * type ; `never` pour un contrôleur sans contrat.
27
+ */
28
+ export type RealtimeContractOf<T> = T extends abstract new (...args: never[]) => {
29
+ readonly [contractType]?: infer C;
30
+ } ? unknown extends C ? never : C extends IRealtimeContract ? C : never : never;
31
+ /**
32
+ * Socle commun des deux formes de {@link ContractRealtimeController} : l'étiquette
33
+ * de contrat et les canaux annoncés. Les actions y gardent le défaut de la base
34
+ * (`{}`) — un contrat SANS `serverMethods` n'a rien à écrire.
35
+ *
36
+ * Ne s'étend pas directement : on étend {@link ContractRealtimeController}, qui
37
+ * choisit cette forme ou la forme stricte selon le contrat.
38
+ *
39
+ * @typeParam C - le contrat de l'application.
40
+ */
41
+ export declare abstract class ContractRealtimeControllerBase<C extends IRealtimeContract> extends RealtimeController<ContractServerEmit<C>, ContractServerActions<C>, ContractServerListen<C>> {
42
+ /**
43
+ * Étiquette de TYPE : relie la classe à son contrat pour que `@RealtimePolicy`
44
+ * le retrouve. `declare` — aucun champ n'existe à l'exécution.
45
+ */
46
+ readonly [contractType]?: C;
47
+ /** Canaux annoncés au handshake, contraints aux noms de `serverToClient`. */
48
+ protected realtimeChannels(): EventNames<ContractServerEmit<C>>[];
49
+ }
50
+ /**
51
+ * Forme stricte de {@link ContractRealtimeController}, retenue quand le contrat
52
+ * déclare des `serverMethods` : `realtimeActions()` y devient ABSTRAITE et typée
53
+ * par le contrat — la seule construction qui fait d'une méthode OUBLIÉE une
54
+ * erreur de compilation (un décorateur ne voit jamais la classe entière).
55
+ *
56
+ * Ne s'étend pas directement : on étend {@link ContractRealtimeController}.
57
+ *
58
+ * @typeParam C - le contrat de l'application.
59
+ */
60
+ export declare abstract class ContractRealtimeControllerStrict<C extends IRealtimeContract> extends ContractRealtimeControllerBase<C> {
61
+ /**
62
+ * Les méthodes du contrat servies par ce contrôleur — toutes, et par cette
63
+ * table SEULE : une action servie ici ne se décore pas en plus de
64
+ * `@RealtimeAction` (handler fabriqué puis écrasé à chaque connexion). Sa
65
+ * politique se pose sur la classe, par `@RealtimePolicy`.
66
+ *
67
+ * Appelée à chaque connexion. Des handlers qui n'ont pas besoin de `this` se
68
+ * rangent dans une table CONSTANTE du module, rendue telle quelle : aucune
69
+ * allocation par connexion.
70
+ */
71
+ protected abstract realtimeActions(): ContractRealtimeMethodHandlers<C>;
72
+ }
73
+ /**
74
+ * Vrai quand le contrat `C` ne déclare aucune méthode serveur. Le tuple empêche
75
+ * la distribution du conditionnel sur `never`.
76
+ */
77
+ type HasNoServerMethods<C extends IRealtimeContract> = [
78
+ ContractActionNames<ContractServerMethods<C>>
79
+ ] extends [never] ? true : false;
80
+ /**
81
+ * L'instance d'un contrôleur de contrat : forme stricte si `C` déclare des
82
+ * `serverMethods`, socle sinon.
83
+ *
84
+ * @typeParam C - le contrat de l'application.
85
+ */
86
+ export type ContractRealtimeController<C extends IRealtimeContract> = HasNoServerMethods<C> extends true ? ContractRealtimeControllerBase<C> : ContractRealtimeControllerStrict<C>;
87
+ /**
88
+ * Le constructeur de {@link ContractRealtimeController} : celui de la forme
89
+ * stricte, dont l'instance se lit par le contrat. Les statiques (`scope`,
90
+ * `assertScope`) restent ceux de la classe.
91
+ */
92
+ type ContractRealtimeControllerConstructor = Omit<typeof ContractRealtimeControllerStrict, "prototype"> & (abstract new <C extends IRealtimeContract>(...args: ConstructorParameters<typeof RealtimeController>) => ContractRealtimeController<C>);
93
+ /**
94
+ * Base d'un contrôleur qui IMPLÉMENTE un contrat temps réel — la porte du
95
+ * serveur. Le contrat s'écrit UNE fois, sur `extends` ; la direction du serveur
96
+ * en est dérivée (il émet `serverToClient`, écoute `clientToServer`, appelle
97
+ * `clientMethods`) et il DOIT servir chaque méthode de `serverMethods`.
98
+ *
99
+ * Quand le contrat déclare des `serverMethods`, `realtimeActions()` est
100
+ * abstraite et typée par lui : une méthode OUBLIÉE ne compile pas. Un contrat
101
+ * SANS `serverMethods` (tableau de bord, chat qui ne fait que diffuser) n'a rien
102
+ * à écrire. Les entrées client (`realtimeInbound()`) restent facultatives canal
103
+ * par canal : accepter une entrée est une permission, pas une promesse. Les
104
+ * politiques d'accès se posent sur la classe par `@RealtimePolicy`, qui lit le
105
+ * contrat sur cette classe.
106
+ *
107
+ * Le fichier du contrat, fait de types seuls, est partagé avec le navigateur, qui
108
+ * entre par `NodefonySocket.fromContract<C>()`.
109
+ *
110
+ * Un contrat qui déclare des `serverRoutes` (routes du pont, typées côté client
111
+ * par le schéma que la route valide) se double d'une table statique de VALEURS,
112
+ * `static readonly serverRoutes = { … } satisfies ContractRouteSchemas<C>` : le
113
+ * compilateur y exige chaque route du contrat avec son schéma, et le démarrage
114
+ * vérifie que chacune est montée, servie par la socket et valide LE MÊME schéma
115
+ * (`BootConfigurationError` sinon). Sans cette table, aucun contrôle.
116
+ *
117
+ * @typeParam C - le contrat de l'application.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * @RealtimePolicy({ "chat:send": { roles: ["ROLE_USER"] } })
122
+ * class ChatRt extends ContractRealtimeController<ChatContract> {
123
+ * protected realtimeActions() {
124
+ * return { "chat:send": (p: { text: string }) => ({ id: this.store(p.text) }) };
125
+ * }
126
+ * }
127
+ *
128
+ * class DashRt extends ContractRealtimeController<DashContract> {} // sans serverMethods
129
+ *
130
+ * class ChatRoutesRt extends ContractRealtimeController<ChatContract> {
131
+ * static readonly serverRoutes = {
132
+ * "POST /chat/send": { body: SendBody }, // le schéma de la route, pas une copie
133
+ * } satisfies ContractRouteSchemas<ChatContract>;
134
+ * }
135
+ * ```
136
+ */
137
+ export declare const ContractRealtimeController: ContractRealtimeControllerConstructor;
138
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { RealtimeClosedReason } from "nodefony";
2
+ import type { ChannelFrame } from "./RealtimeHub.js";
3
+ /**
4
+ * Ce que la poignée d'une connexion demande au contrôleur qui la sert — imposer
5
+ * un abonnement, y mettre fin. Implémenté par `RealtimeController`, une fois par
6
+ * connexion.
7
+ */
8
+ export interface IRealtimeConnectionHost {
9
+ /** Lance l'abonnement imposé à `channel` ; le verdict passe par {@link RealtimeConnection.settleImposed}. */
10
+ impose(channel: string): void;
11
+ /**
12
+ * Met fin à l'abonnement de cette connexion à `channel` (hub ET état de la
13
+ * connexion) et le lui notifie (`realtime:closed`).
14
+ *
15
+ * @returns `false` si la connexion n'est pas abonnée à ce canal.
16
+ */
17
+ end(channel: string, reason: RealtimeClosedReason): boolean;
18
+ /** Écrit une trame partagée, encodée pour le codec de cette connexion. */
19
+ send(frame: ChannelFrame): void;
20
+ }