@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,71 @@
1
+ /** Rendu par {@link AdmissionBudget.acquire} quand une place est réservée. */
2
+ export declare const ADMISSION_GRANTED = -1;
3
+ /** Rendu par {@link AdmissionBudget.authenticateWithin} quand l'authentification n'a pas répondu à temps. */
4
+ export declare const AUTHENTICATE_TIMED_OUT: unique symbol;
5
+ /** Bornes des admissions, lues dans la configuration realtime (`authenticateTimeoutMs`, `allowTimeoutMs`, `maxPendingAllowsPerConnection`, `maxPendingAllowsTotal`). */
6
+ export interface IAdmissionLimits {
7
+ readonly authenticateTimeoutMs: number;
8
+ readonly allowTimeoutMs: number;
9
+ readonly maxPendingAllowsPerConnection: number;
10
+ readonly maxPendingAllowsTotal: number;
11
+ }
12
+ /**
13
+ * Budget des admissions d'UN exemplaire — appels d'`allow` en vol, coupe-circuit et compteurs de refus (ADR-0021 §S10 bis point 3, §S9).
14
+ *
15
+ * Partagé par toutes les connexions du hub ; chaque connexion garde sa propre
16
+ * file ({@link ConnectionAdmission}). Aucune horloge n'est lue tant que le
17
+ * coupe-circuit est fermé : une règle synchrone ne paie que deux incréments.
18
+ */
19
+ export declare class AdmissionBudget {
20
+ #private;
21
+ /** Délai de l'authentification à la poignée de main (ADR-0021 §S11). */
22
+ authenticateTimeoutMs: number;
23
+ /** Délai d'un appel d'`allow`, compté depuis la réception du `subscribe`, file comprise. */
24
+ timeoutMs: number;
25
+ /** Appels d'`allow` en vol par connexion ; au-delà, la demande attend son tour. */
26
+ maxPerConnection: number;
27
+ /** Applique les bornes de la configuration (boot, cold path). */
28
+ configure(limits: IAdmissionLimits): void;
29
+ /**
30
+ * Réserve une place pour UN appel d'`allow`.
31
+ *
32
+ * @returns {@link ADMISSION_GRANTED} si la place est prise ; sinon la demande
33
+ * est comptée dans `allowSaturatedTotal` et la valeur rendue est le délai à
34
+ * annoncer au client (`retryAfterMs`, coupe-circuit ouvert), ou `0` quand il
35
+ * n'est pas connu (plafond global atteint).
36
+ */
37
+ acquire(): number;
38
+ /** Rend la place d'un appel terminé, dépassé ou abandonné à la fermeture. */
39
+ release(): void;
40
+ /** Un appel d'`allow` n'a pas répondu dans son délai : compté, et porté au coupe-circuit. */
41
+ recordTimeout(now: number): void;
42
+ /** Une demande a épuisé son délai dans la file, sans appel d'`allow`. */
43
+ recordQueueExpired(): void;
44
+ /** Délai restant du coupe-circuit à `now`, ou `undefined` s'il est fermé. */
45
+ retryAfter(now: number): number | undefined;
46
+ /** Faut-il journaliser cette cause maintenant ? Vrai au plus une fois par fenêtre et par cause. */
47
+ shouldWarn(kind: "timeout" | "throw" | "authenticate", now: number): boolean;
48
+ /**
49
+ * Borne l'authentification d'une poignée de main par `authenticateTimeoutMs` (ADR-0021 §S11).
50
+ *
51
+ * Une identité arrivée après le délai est IGNORÉE : la promesse rendue est
52
+ * déjà réglée sur {@link AUTHENTICATE_TIMED_OUT}, et un rejet tardif ne lève
53
+ * rien (la promesse d'origine garde son gestionnaire). Le minuteur est coupé
54
+ * dès la réponse et ne retient pas le processus à l'arrêt. Chaque dépassement
55
+ * compte une poignée de main dans `authenticateTimedOutTotal`.
56
+ *
57
+ * @param pending - la promesse rendue par `authenticate()`
58
+ * @returns le jeton, ou {@link AUTHENTICATE_TIMED_OUT} au dépassement
59
+ */
60
+ authenticateWithin<T>(pending: Promise<T>): Promise<T | typeof AUTHENTICATE_TIMED_OUT>;
61
+ /** Appels d'`allow` en vol sur l'exemplaire. */
62
+ get pending(): number;
63
+ /** Cumul `allowTimedOutTotal` — unité : un appel d'`allow`. */
64
+ get timedOutTotal(): number;
65
+ /** Cumul `allowSaturatedTotal` — unité : une demande d'abonnement. */
66
+ get saturatedTotal(): number;
67
+ /** Cumul `authenticateTimedOutTotal` — unité : une poignée de main. */
68
+ get authenticateTimedOutTotal(): number;
69
+ /** Remet compteurs, coupe-circuit et bornes à leurs défauts (hub vidé). */
70
+ reset(): void;
71
+ }
@@ -0,0 +1,41 @@
1
+ import type { ChannelAdmissionClass, IChannelAdmission } from "nodefony";
2
+ import type RealtimeHub from "./RealtimeHub.js";
3
+ /**
4
+ * Résout l'instance d'une classe d'admission pour un canal — fournie par le module realtime au démarrage, absente à la poignée de main.
5
+ */
6
+ export type AdmissionResolver = (admissionClass: ChannelAdmissionClass, channel: string) => IChannelAdmission;
7
+ /**
8
+ * Inscrit au hub les politiques déclarées par une classe de contrôleur — une fois par classe et par hub, et seulement si l'inscription entière réussit.
9
+ *
10
+ * Appelée pour chaque contrôleur connu au démarrage du noyau, puis à la
11
+ * poignée de main pour une classe qui y aurait échappé : celle-ci ne paie alors
12
+ * qu'un `has`. Une politique refusée ne marque PAS la classe — elle est refusée
13
+ * de nouveau à la poignée de main suivante plutôt que de laisser le canal
14
+ * ouvert.
15
+ *
16
+ * @param hub - le hub qui porte le registre.
17
+ * @param ctor - constructeur du contrôleur.
18
+ * @param resolveAdmission - résolution des classes d'admission ; `null` hors
19
+ * démarrage, et le hub refuse alors une politique qui en désigne une.
20
+ * Chaque nom est déclaré au hub SORTE PAR SORTE (lecture, écriture), sans
21
+ * héritage : le hub les fusionne avec les motifs de toutes les classes
22
+ * (ADR-0022 §6, décision 1 quater). Deux classes qui déclarent la même sorte
23
+ * différemment sous un même nom refusent le démarrage : la seconde effaçait la
24
+ * première sans un mot.
25
+ *
26
+ * @throws BootConfigurationError Politique illisible ou contredite, `allow`
27
+ * posé sur une action ou sur une écriture distincte de la lecture, classe
28
+ * d'admission non résolue, ou deux classes en désaccord sur un nom.
29
+ */
30
+ export declare function registerControllerPolicies(hub: RealtimeHub, ctor: object, resolveAdmission: AdmissionResolver | null): void;
31
+ /**
32
+ * Fabrique la résolution des classes d'admission du démarrage : portée singleton exigée, instance du conteneur réutilisée, sinon construite UNE fois par l'injecteur.
33
+ *
34
+ * @param get - l'instance déjà construite d'une classe : le démarrage la lit
35
+ * dans le conteneur du noyau par sa clé (`null` tant que la classe n'y a
36
+ * jamais été rangée), le harnais de test parmi les instances que le test lui
37
+ * donne. `undefined` → construite par l'injecteur.
38
+ * @param log - journal du module (une ligne par canal résolu).
39
+ * @returns le résolveur, qui mémorise l'instance de chaque classe.
40
+ */
41
+ export declare function createAdmissionResolver(get: (key: string | null, admissionClass: ChannelAdmissionClass) => unknown, log: (message: string) => void): AdmissionResolver;
@@ -0,0 +1,112 @@
1
+ import { type IChannelAdmission, type IChannelPolicies } from "nodefony";
2
+ import type { IChannelPolicy } from "../../interfaces/IChannelPolicy.js";
3
+ import type { IChannelAdmissionRule } from "./connectionAdmission.js";
4
+ /**
5
+ * Politiques qui s'appliquent à un nom, résolues : la LECTURE, l'ÉCRITURE quand elle en diffère, et — quand la lecture porte `allow` — la règle d'admission prête à appeler.
6
+ *
7
+ * Satisfait {@link IChannelPolicies} : le hub la rend telle quelle au juge, sans
8
+ * allocation.
9
+ */
10
+ export interface IChannelPolicyEntry extends IChannelPolicies {
11
+ readonly admission: IChannelAdmissionRule | null;
12
+ }
13
+ /**
14
+ * Ce qu'UNE déclaration (nom exact ou motif) pose sur un nom, par sorte — et sa vue seule, calculée une fois.
15
+ *
16
+ * `read` et `write` sont `null` quand cette déclaration ne pose pas la sorte :
17
+ * la résolution la cherche alors dans une déclaration plus générale
18
+ * (ADR-0022 §6, décision 1 quater). `fallback` est le défaut fermé d'une action
19
+ * sans politique : son écriture tant que rien d'exact ne déclare le nom et
20
+ * qu'aucun motif ne pose d'écriture.
21
+ */
22
+ export interface IPolicyDeclaration {
23
+ readonly read: IChannelPolicy | null;
24
+ readonly write: IChannelPolicy | null;
25
+ readonly fallback: IChannelPolicy | null;
26
+ /** Instance de la classe d'admission désignée par l'une des politiques, gardée pour une fusion ultérieure. */
27
+ readonly instance: IChannelAdmission | undefined;
28
+ /** Vue de cette déclaration seule, ou `null` si elle ne pose rien. */
29
+ readonly self: IChannelPolicyEntry | null;
30
+ }
31
+ /** Déclaration portée par un MOTIF de nom (`chat:room:*`), compilée et pesée. */
32
+ export interface IPatternDeclaration extends IPolicyDeclaration {
33
+ readonly source: string;
34
+ readonly re: RegExp;
35
+ /** Longueur du littéral (`*` retirés) : le plus spécifique d'abord. */
36
+ readonly weight: number;
37
+ }
38
+ /**
39
+ * Règle d'admission prête à appeler d'une politique, ou `null` sans `allow`.
40
+ *
41
+ * Une flèche se retient telle quelle. Une classe exige son instance — résolue au
42
+ * démarrage, jamais ici : le hub n'a pas d'injecteur, et résoudre à la première
43
+ * poignée de main laisserait sans politique un exemplaire qui n'émet que des
44
+ * webhooks. La méthode est liée une fois, et la règle se construit une fois :
45
+ * le `subscribe` n'alloue rien et n'appelle qu'une fonction.
46
+ *
47
+ * @throws BootConfigurationError La politique désigne une classe d'admission et
48
+ * aucune instance de CETTE classe n'est fournie.
49
+ */
50
+ export declare function admissionRuleOf(name: string, policy: IChannelPolicy, admission: IChannelAdmission | undefined): IChannelAdmissionRule | null;
51
+ /**
52
+ * Construit une déclaration et sa vue seule : lecture déclarée, sinon l'écriture, sinon le défaut d'action ; écriture gardée seulement si elle exige autre chose que la lecture.
53
+ *
54
+ * @param name - nom exact ou motif (pour les messages).
55
+ * @param read - lecture déclarée, ou `null`.
56
+ * @param write - écriture déclarée, ou `null`.
57
+ * @param fallback - défaut fermé d'une action, ou `null`.
58
+ * @param admission - instance de la classe d'admission quand la politique qui
59
+ * sert de lecture en désigne une.
60
+ */
61
+ export declare function declarationOf(name: string, read: IChannelPolicy | null, write: IChannelPolicy | null, fallback: IChannelPolicy | null, admission: IChannelAdmission | undefined): IPolicyDeclaration;
62
+ /**
63
+ * Politique INTÉGRÉE du canal personnel `user:<id>` (ADR-0022 §6, D1) — connexion authentifiée, admise sur SON canal seulement.
64
+ *
65
+ * Constante de module : elle ne s'inscrit nulle part, donc aucun `clear()`,
66
+ * aucun ordre de démarrage, aucun oubli ne peut la retirer. Elle sert de REPLI
67
+ * de LECTURE aux noms `user:…` : seule une déclaration qui vise `user:`
68
+ * (nom exact, ou motif dont le littéral commence par `user:`) la remplace — un
69
+ * motif attrape-tout (`*`) ne retire pas la garde « son canal seulement ».
70
+ */
71
+ export declare const USER_CHANNEL_DECLARATION: IPolicyDeclaration;
72
+ /**
73
+ * Résout les politiques d'un nom (ADR-0022 §6, décision 1 quater) : la LECTURE appartient à la déclaration la plus spécifique qui déclare quelque chose sur ce nom ; l'ÉCRITURE vient de la plus spécifique qui déclare une écriture.
74
+ *
75
+ * L'ordre de spécificité : le nom exact, puis les motifs du plus spécifique au
76
+ * plus général. Pourquoi deux règles et non une :
77
+ * - la LECTURE reste à la déclaration propriétaire du nom (déclarée, sinon
78
+ * héritée de sa propre écriture) : une commande `task:start` déclarée par
79
+ * `@RealtimeInbound` sous une famille `task:*` à `allow` ne devient pas un
80
+ * canal à membres, et une écriture exacte ne voit pas sa lecture s'ouvrir par
81
+ * un motif plus général ;
82
+ * - l'ÉCRITURE descend jusqu'au motif qui en déclare une : elle s'ajoute à la
83
+ * lecture (une trame entrante exige les deux), elle ne peut que durcir — une
84
+ * écriture de famille (`mix:*` réservée) n'est plus perdue sur un nom exact
85
+ * qui ne déclare que sa lecture.
86
+ *
87
+ * Ensuite : un nom `user:…` sans déclaration qui le vise garde la politique
88
+ * intégrée du canal personnel (un motif qui ne vise pas `user:` est ignoré) ;
89
+ * une action sans politique (nom exact qui ne déclare rien) prend la lecture du
90
+ * motif qui la couvre et le défaut fermé pour écriture — un motif de lecture
91
+ * public ne l'ouvre donc pas.
92
+ *
93
+ * Rend la vue d'une déclaration quand une seule suffit (aucune allocation) ;
94
+ * n'alloue que si l'écriture vient d'une autre déclaration que la lecture.
95
+ *
96
+ * @param name - nom résolu.
97
+ * @param exact - déclaration exacte de ce nom, ou `undefined`.
98
+ * @param patterns - motifs, triés du plus spécifique au plus général, ou `null`.
99
+ * @returns les politiques, ou `null` si aucune ne couvre le nom.
100
+ */
101
+ export declare function resolveDeclaredPolicies(name: string, exact: IPolicyDeclaration | undefined, patterns: readonly IPatternDeclaration[] | null): IChannelPolicyEntry | null;
102
+ /**
103
+ * Défaut de l'ensemble des déclarations qui refuse le démarrage, ou `null` : une ÉCRITURE à `allow` qui s'appliquerait sous une lecture différente (son `allow` ne serait évalué nulle part), ou — face à un verrou qui ne juge pas l'écriture — une écriture qui diffère d'une lecture qu'elle peut rencontrer.
104
+ *
105
+ * Cold path (inscription d'une politique, pose du verrou) : toutes les paires.
106
+ *
107
+ * @param exact - déclarations exactes, par nom.
108
+ * @param patterns - déclarations par motif.
109
+ * @param writeJudged - le verrou en place sait juger l'écriture (ou aucun verrou).
110
+ * @returns le message du refus, ou `null`.
111
+ */
112
+ export declare function policyDeclarationsDefect(exact: ReadonlyMap<string, IPolicyDeclaration> | null, patterns: readonly IPatternDeclaration[] | null, writeJudged: boolean): string | null;
@@ -0,0 +1,155 @@
1
+ import type { IChannelPolicy, IPresenceView, IRealtimePresenceCount, IRealtimePresenceList, RealtimePresenceFrame } from "nodefony";
2
+ import type { IPresenceChannelDelta, IPresenceChannelState } from "../backplane/presenceMessage.js";
3
+ import type { ChannelFrame } from "./RealtimeHub.js";
4
+ import type { IPresenceAggregate } from "./remotePresence.js";
5
+ /**
6
+ * Fenêtre de regroupement des changements de présence vers le navigateur (ms) — front montant : le premier changement d'un canal calme part tout de suite, les suivants attendent la fin de la fenêtre (ADR-0025 P2 bis).
7
+ */
8
+ export declare const PRESENCE_WINDOW_MS = 250;
9
+ /** Seuil par défaut d'identités nommées au-delà duquel la trame ne porte que les nombres (ADR-0025 P2 bis). */
10
+ export declare const DEFAULT_PRESENCE_MAX_NAMED = 1000;
11
+ /** Réglages RÉSOLUS de la présence d'un canal — défauts appliqués. */
12
+ export interface IPresenceSettings {
13
+ /** Délai de grâce d'un départ (ms), `0` = départs exacts. */
14
+ readonly grace: number;
15
+ /** Identités nommées au-delà desquelles la trame ne porte que les nombres. */
16
+ readonly maxNamed: number;
17
+ }
18
+ /**
19
+ * Ce qu'un observateur serveur lit de la présence d'un canal (`hub.presence()`, ADR-0025 P3) — les membres TOUJOURS, quel que soit le seuil de la trame.
20
+ *
21
+ * La forme est celle du cœur (`IPresenceView`), que le navigateur lit aussi : serveur et client voient le même objet.
22
+ */
23
+ export type IChannelPresenceView = IPresenceView;
24
+ /**
25
+ * Défaut de la valeur de la clé `presence` d'une politique, ou `null` si elle est recevable — en clair, pour que le refus au démarrage dise QUOI corriger.
26
+ *
27
+ * @param value - valeur de `policy.presence` (`unknown` : une politique se lit
28
+ * depuis une métadonnée publique).
29
+ */
30
+ export declare function presenceDefect(value: unknown): string | null;
31
+ /**
32
+ * Réglages résolus de la présence qu'une politique déclare, ou `null` si elle n'en déclare pas.
33
+ *
34
+ * Précondition : la politique a passé {@link presenceDefect}.
35
+ */
36
+ export declare function presenceSettingsOf(policy: IChannelPolicy | null | undefined): IPresenceSettings | null;
37
+ /**
38
+ * Deux politiques déclarent-elles la même présence ? Défauts appliqués : `true` et `{ grace: 0 }` sont la même.
39
+ *
40
+ * Sert à refuser deux déclarations d'un même nom qui ne diffèrent que par la
41
+ * présence — une seule s'appliquerait. Démarrage seulement.
42
+ */
43
+ export declare function samePresence(a: IChannelPolicy, b: IChannelPolicy): boolean;
44
+ /**
45
+ * État de présence LOCAL d'un canal à présence (ADR-0025 P2 bis, P5) — identités nommées et onglets, connexions non nommées, version, changements en attente de la fenêtre, départs en grâce.
46
+ *
47
+ * Alloué au premier client d'un canal qui déclare `presence`, jamais pour un
48
+ * autre canal. Ne connaît ni l'horloge ni le protocole : le hub lui passe
49
+ * l'instant et fabrique les trames. Les changements portent des valeurs
50
+ * ABSOLUES (nombre d'onglets, départ), donc rejouer un changement déjà inclus
51
+ * dans une liste ne change rien — c'est ce qui permet de servir à un nouvel
52
+ * abonné la liste prise au dernier envoi, encodée une fois.
53
+ *
54
+ * Sur un canal DIFFUSÉ entre pods, c'est aussi l'agrégat de la vue des pairs
55
+ * ({@link IPresenceAggregate}) : les compteurs portent le TOTAL (local + pairs),
56
+ * et une table à part garde la seule part locale — ce que ce pod annonce à ses
57
+ * pairs, en deltas par lot. Ailleurs, cette table n'existe pas.
58
+ */
59
+ export declare class ChannelPresence implements IPresenceAggregate {
60
+ #private;
61
+ readonly channel: string;
62
+ readonly settings: IPresenceSettings;
63
+ /** Canal diffusé entre pods : sa part locale part sur le bus, la vue des pairs s'y ajoute. */
64
+ readonly distributed: boolean;
65
+ /** Fin de la fenêtre de regroupement en cours (horloge monotone, ms). */
66
+ windowEnd: number;
67
+ /** Liste complète encodée, servie telle quelle jusqu'au prochain envoi. */
68
+ listFrame: ChannelFrame | null;
69
+ /** Le canal est-il encore tenu par le hub ? `false` après son retrait. */
70
+ live: boolean;
71
+ /**
72
+ * Fin de la synchronisation initiale avec les pairs (ms monotone) — la vue
73
+ * n'est que locale tant que la demande d'état du premier abonné n'a pas eu le
74
+ * temps d'être servie ; `0` = synchronisée (ou canal non diffusé).
75
+ */
76
+ syncUntil: number;
77
+ /**
78
+ * @param distributed - le canal est diffusé entre pods (bus branché, préfixe
79
+ * diffusé) : la part locale est tenue à part pour le bus.
80
+ */
81
+ constructor(channel: string, settings: IPresenceSettings, distributed?: boolean);
82
+ /** La part locale a-t-elle changé depuis le dernier lot ? */
83
+ get busDirty(): boolean;
84
+ /** La vue est-elle connue incomplète (synchronisation initiale, trou chez un pair) ? */
85
+ get partial(): boolean;
86
+ /** Version courante de la vue (+1 par trame de changements). */
87
+ get version(): number;
88
+ /** Des changements attendent-ils un envoi ? */
89
+ get dirty(): boolean;
90
+ /**
91
+ * Une connexion entre.
92
+ *
93
+ * @param key - identité nommée, ou `null` pour une connexion non nommée.
94
+ */
95
+ join(key: string | null): void;
96
+ /**
97
+ * Une connexion sort.
98
+ *
99
+ * Un départ nommé attend la grâce du canal ; un départ non nommé est décompté
100
+ * tout de suite (rien ne reconnaîtrait son retour).
101
+ *
102
+ * @param key - même valeur qu'à {@link ChannelPresence.join}.
103
+ * @param now - instant monotone (ms), pour la grâce.
104
+ */
105
+ leave(key: string | null, now: number): void;
106
+ /** Publie les départs dont la grâce est écoulée. */
107
+ expire(now: number): void;
108
+ /** Prochaine échéance de grâce (ms monotone), `Infinity` sans départ retenu — O(1). */
109
+ nextDeadline(): number;
110
+ /**
111
+ * Trame complète à la version COURANTE — la liste, ou les nombres seuls au-delà du seuil.
112
+ *
113
+ * Une liste ne prend jamais de numéro : servie à un nouvel abonné, elle ferait
114
+ * voir un trou à tous les autres (ADR-0025 P2 bis).
115
+ */
116
+ listParams(): IRealtimePresenceList | IRealtimePresenceCount;
117
+ /**
118
+ * Consomme les changements en attente et rend la trame à envoyer à tous les membres, ou `null` s'il n'y en a pas.
119
+ *
120
+ * Sous le seuil : les changements, version +1. Au-delà : les nombres seuls,
121
+ * version inchangée (trame complète). Au retour sous le seuil : la liste.
122
+ * Toute trame envoyée périme la liste encodée.
123
+ */
124
+ takeChanges(): RealtimePresenceFrame | null;
125
+ /** Vue de l'observateur serveur : les membres toujours, une copie. */
126
+ view(): IChannelPresenceView;
127
+ /**
128
+ * Pose l'état « incomplet » de la vue ; un changement fait partir une trame (la version suit) et périme la liste encodée.
129
+ */
130
+ setPartial(partial: boolean): void;
131
+ /** {@inheritDoc IPresenceAggregate.remoteTabs} */
132
+ remoteTabs(key: string, delta: number): void;
133
+ /** {@inheritDoc IPresenceAggregate.remoteUnnamed} */
134
+ remoteUnnamed(delta: number): void;
135
+ /** {@inheritDoc IPresenceAggregate.remoteLeave} */
136
+ remoteLeave(key: string, tabs: number, now: number): void;
137
+ /**
138
+ * Consomme les changements de la part locale depuis le dernier lot et rend l'entrée du lot, en valeurs absolues — `null` s'ils s'annulent tous.
139
+ */
140
+ takeBusDelta(): IPresenceChannelDelta | null;
141
+ /** État COMPLET de la part locale, pour une réponse d'état (paires à plat). */
142
+ localState(): IPresenceChannelState;
143
+ /**
144
+ * Des départs LOCAUX attendent-ils la fin de leur grâce ? Le dernier client
145
+ * parti d'un pod n'y échappe pas : la présence reste servie au bus jusqu'à
146
+ * l'échéance (ADR-0025 P5), puis se libère.
147
+ */
148
+ get hasLocalGrace(): boolean;
149
+ /** La part locale compte-t-elle quelqu'un ? */
150
+ get hasLocal(): boolean;
151
+ /**
152
+ * Le canal n'est plus servi ici : toute la part locale part à zéro au prochain lot, pour que les pairs ne gardent aucun fantôme.
153
+ */
154
+ release(): void;
155
+ }
@@ -0,0 +1,68 @@
1
+ import type { ChannelAllowFunction, ISubscriberIdentity } from "nodefony";
2
+ import { type AdmissionBudget } from "./admissionBudget.js";
3
+ /**
4
+ * Règle d'admission d'un canal telle que le hub la tient prête — construite UNE fois à l'inscription de la politique, lue au `subscribe`.
5
+ */
6
+ export interface IChannelAdmissionRule {
7
+ /** La règle, flèche ou méthode `allow` déjà liée à son instance. */
8
+ readonly allow: ChannelAllowFunction;
9
+ /** `anonymous: true` de la politique : l'anonyme est présenté à `allow` au lieu d'être refusé d'office. */
10
+ readonly anonymous: boolean;
11
+ }
12
+ /**
13
+ * Ce que la file d'admission demande à sa connexion — ouvrir le canal, le refuser, journaliser. Implémenté par `RealtimeController`, une fois par connexion.
14
+ */
15
+ export interface IAdmissionHost {
16
+ /** Ouvre l'abonnement admis (et l'acquitte). */
17
+ open(channel: string): void;
18
+ /** Rend le refus observable (`realtime:denied`) ; `detail` n'est rédigé qu'hors production. */
19
+ deny(channel: string, reason: "forbidden" | "unavailable", detail: string | undefined, retryAfterMs?: number): void;
20
+ /** Journal d'exploitation (déjà limité en fréquence par l'appelant). */
21
+ warn(message: string): void;
22
+ }
23
+ /**
24
+ * Admission des abonnements d'UNE connexion aux canaux qui déclarent `allow` — appels bornés, file d'attente, délai, verdict (ADR-0021 §S10 et §S10 bis).
25
+ *
26
+ * Coûts, par construction :
27
+ * - une règle qui répond `true`/`false` sans promesse ne crée ni minuteur, ni
28
+ * promesse, ni entrée d'attente ;
29
+ * - au-delà de `maxPendingAllowsPerConnection` appels en vol, la demande ATTEND
30
+ * son tour (jamais `limit` : le rejeu au `welcome` renvoie tous les canaux
31
+ * d'un coup) ; la file reste `null` tant que personne n'attend ;
32
+ * - un seul minuteur par connexion, armé sur l'échéance de tête : les demandes
33
+ * arrivent dans l'ordre de leurs échéances, la plus ancienne non tranchée est
34
+ * donc toujours la prochaine à expirer.
35
+ *
36
+ * Seul `=== true` admet ; une exception ou un rejet valent `forbidden` ; un
37
+ * délai dépassé ou une saturation valent `unavailable`, et un verdict arrivé
38
+ * après coup est ignoré.
39
+ */
40
+ export declare class ConnectionAdmission {
41
+ #private;
42
+ /**
43
+ * @param host - la connexion qui ouvre ou refuse.
44
+ * @param budget - le budget de l'exemplaire (plafond global, coupe-circuit).
45
+ * @param who - identité d'admission gelée de la connexion.
46
+ * @param explain - rédiger le détail des refus (hors production).
47
+ */
48
+ constructor(host: IAdmissionHost, budget: AdmissionBudget, who: ISubscriberIdentity, explain: boolean);
49
+ /** Demandes en attente de verdict — comptées dans le plafond de canaux de la connexion. */
50
+ get unsettled(): number;
51
+ /**
52
+ * Un abonnement à ce canal attend-il déjà son verdict ? S'il avait été annulé par un `unsubscribe`, il est ranimé : le verdict à venir répondra.
53
+ *
54
+ * @returns `true` si la demande est déjà en attente (rien d'autre à faire).
55
+ */
56
+ revive(channel: string): boolean;
57
+ /** `unsubscribe` pendant l'attente : le verdict à venir sera jeté sans réponse. */
58
+ cancel(channel: string): void;
59
+ /**
60
+ * Demande l'admission sur `channel` selon sa règle — l'anonyme est refusé d'office sauf `anonymous: true`.
61
+ *
62
+ * @param channel - nom exact du canal demandé.
63
+ * @param rule - règle résolue par le hub pour ce canal.
64
+ */
65
+ request(channel: string, rule: IChannelAdmissionRule): void;
66
+ /** Fermeture de la connexion : minuteur coupé, places rendues, verdicts à venir jetés. */
67
+ close(): void;
68
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Une route montée, vue par le contrôle des routes de contrat : son chemin
3
+ * complet, les méthodes qu'elle accepte et les schémas qu'elle valide.
4
+ */
5
+ export interface IMountedBridgeRoute {
6
+ /** Chemin complet, préfixe du contrôleur compris (`/chat/send`). */
7
+ readonly path: string;
8
+ /** Méthodes acceptées, en capitales ; `undefined` = aucune déclarée (tous les verbes HTTP, jamais la socket). */
9
+ readonly methods: ReadonlySet<string> | undefined;
10
+ /** Schémas d'entrée de l'action (`schema` de la route), ou `null`. */
11
+ readonly schema: {
12
+ readonly body?: unknown;
13
+ readonly query?: unknown;
14
+ readonly params?: unknown;
15
+ } | null;
16
+ }
17
+ /**
18
+ * Lit la table `serverRoutes` qu'une classe de contrôleur déclare en statique —
19
+ * `null` quand elle n'en déclare pas (aucun contrôle).
20
+ *
21
+ * @param ctor - la classe du contrôleur.
22
+ * @returns la table, ou `null`.
23
+ * @throws BootConfigurationError si la table n'est pas un objet.
24
+ */
25
+ export declare function readContractRoutes(ctor: object): Record<string, unknown> | null;
26
+ /**
27
+ * Vérifie, au démarrage, que chaque route du contrat est SERVIE par le pont :
28
+ * une route montée au même chemin, qui accepte le verbe par la socket, et dont
29
+ * les schémas sont LES MÊMES OBJETS que ceux que la table déclare. Coût nul par
30
+ * requête : la vérification n'a lieu qu'une fois.
31
+ *
32
+ * Sans elle, un contrat qui type `chat.post("/chat/send", …)` compilerait
33
+ * contre une route absente, servie en HTTP seulement, ou validant un autre
34
+ * schéma — et l'erreur n'apparaîtrait qu'au premier appel.
35
+ *
36
+ * @param owner - nom de la classe qui déclare la table (pour le message).
37
+ * @param table - la table `serverRoutes` (`"<VERBE> <chemin>"` → `{ body?, query? }`).
38
+ * @param mounted - les routes montées.
39
+ * @throws BootConfigurationError à la première entrée qui n'est pas servie ;
40
+ * le message nomme la classe, l'entrée et la cause.
41
+ */
42
+ export declare function assertContractRoutes(owner: string, table: Record<string, unknown>, mounted: readonly IMountedBridgeRoute[]): void;
@@ -23,3 +23,9 @@
23
23
  export declare function deniedDetail(env: string | undefined | null, detail: string): {
24
24
  detail?: string;
25
25
  };
26
+ /**
27
+ * Le détail d'un refus part-il au client dans ce mode ? Hors production seulement — la règle de {@link deniedDetail}, lue AVANT de rédiger la phrase : en production, aucune chaîne n'est composée pour être jetée.
28
+ *
29
+ * @param env - le mode du kernel serveur, s'il est connu ; une absence vaut production.
30
+ */
31
+ export declare function explainsDenials(env: string | undefined | null): boolean;
@@ -0,0 +1,66 @@
1
+ import type { IJsonRpcNotification, IJsonRpcRequest } from "nodefony";
2
+ import type { IRealtimeConnectionState } from "../../interfaces/IRealtimeConnectionState.js";
3
+ import type { RealtimeHub } from "./RealtimeHub.js";
4
+ /**
5
+ * Verdicts du verrou retenus par connexion, au plus (ADR-0022 §6, décision 1 ter).
6
+ *
7
+ * Borne la table qu'un client remplit en poussant sur des canaux entrants : au-delà,
8
+ * le verrou est consulté à chaque trame, il n'est jamais contourné. Une admission
9
+ * par `allow` n'y est pas soumise — elle est déjà bornée par le plafond de canaux
10
+ * de la connexion.
11
+ */
12
+ export declare const MAX_RETAINED_FRAME_VERDICTS = 64;
13
+ /**
14
+ * Retire TOUTE admission d'un canal à la connexion — marque de sa règle ET verdict retenu du verrou — quand le serveur l'en éjecte ou le termine (ADR-0022 §6 décision 3).
15
+ *
16
+ * Plus fort que {@link releaseRuleAdmission}, qui garde le verdict du verrou :
17
+ * après une éjection, une trame entrante sur ce canal repasse par le verrou.
18
+ *
19
+ * @param state - état de la connexion.
20
+ * @param channel - canal dont la connexion est retirée.
21
+ */
22
+ export declare function revokeAdmission(state: IRealtimeConnectionState, channel: string): void;
23
+ /**
24
+ * Retire l'admission d'un canal à la fin de son abonnement quand elle en dépendait — celle d'un canal à `allow`, dont l'abonnement est la condition.
25
+ *
26
+ * Un verdict du verrou sur un canal SANS `allow` ne tient qu'à l'identité et
27
+ * survit au désabonnement ; sur un canal à `allow`, il ne valait que le temps
28
+ * de l'abonnement (ADR-0022 §6, décisions 1 et 1 quater).
29
+ *
30
+ * @param state - état de la connexion.
31
+ * @param channel - canal quitté.
32
+ * @param hub - hub qui porte les règles d'admission.
33
+ */
34
+ export declare function releaseRuleAdmission(state: IRealtimeConnectionState, channel: string, hub: RealtimeHub): void;
35
+ /**
36
+ * Oublie les verdicts du verrou retenus par la connexion — leur politique vient
37
+ * de changer. Ceux des canaux à `allow` compris : une écriture inscrite après
38
+ * coup doit être jugée à la trame suivante, abonnement ou pas. `allow`, lui,
39
+ * n'est pas redemandé — l'abonnement reste.
40
+ *
41
+ * @param state - état de la connexion.
42
+ */
43
+ export declare function dropRetainedVerdicts(state: IRealtimeConnectionState): void;
44
+ /**
45
+ * Juge une trame entrante que la connexion n'a pas admise d'office (ADR-0022 §6, décision 1 ter).
46
+ *
47
+ * Dans l'ordre :
48
+ * - une REQUÊTE (action, `api.request`) passe par le verrou, toujours ;
49
+ * - `ping` et `unsubscribe` passent sans règle ;
50
+ * - `subscribe` passe par le verrou (`allow` est jugé après, à l'ouverture) ;
51
+ * - un nom hérité du prototype de la table entrante est refusé ;
52
+ * - un canal entrant à `allow` exige l'abonnement de CETTE connexion (`allow`
53
+ * n'est jamais rappelé), puis passe par le verrou comme les autres ;
54
+ * - un canal entrant passe par le verrou — lecture ET écriture —, dont le « oui »
55
+ * est retenu ;
56
+ * - toute autre notification passe par le verrou.
57
+ *
58
+ * Le premier test du contrôleur (`admitted[method] === true`) a déjà écarté les trames
59
+ * admises : rien ici n'est sur le chemin d'une trame chaude.
60
+ *
61
+ * @param state - état de la connexion.
62
+ * @param frame - trame déjà classée par le pair.
63
+ * @param hub - hub qui porte le verrou et les règles.
64
+ * @returns `true` si la trame peut être distribuée.
65
+ */
66
+ export declare function gateFrame(state: IRealtimeConnectionState, frame: IJsonRpcRequest | IJsonRpcNotification, hub: RealtimeHub): boolean;