@nodefony/realtime 10.0.0-beta.6 → 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 (62) hide show
  1. package/README.md +5 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +15 -3
  4. package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
  5. package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
  6. package/dist/nodefony/src/backplane/controlOrder.js +34 -5
  7. package/dist/nodefony/src/backplane/envelope.js +422 -45
  8. package/dist/nodefony/src/backplane/ingress.js +93 -0
  9. package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
  10. package/dist/nodefony/src/backplane/publishQueue.js +97 -3
  11. package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
  12. package/dist/nodefony/src/server/RealtimeController.js +87 -44
  13. package/dist/nodefony/src/server/RealtimeHub.js +631 -127
  14. package/dist/nodefony/src/server/admissionBudget.js +8 -8
  15. package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
  16. package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
  17. package/dist/nodefony/src/server/channelPresence.js +488 -0
  18. package/dist/nodefony/src/server/contractRoutes.js +3 -13
  19. package/dist/nodefony/src/server/frameAdmission.js +27 -35
  20. package/dist/nodefony/src/server/presenceBus.js +486 -0
  21. package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
  22. package/dist/nodefony/src/server/remotePresence.js +541 -0
  23. package/dist/nodefony/src/server/subscribedAck.js +3 -0
  24. package/dist/nodefony/src/service/RealtimeService.js +9 -0
  25. package/dist/nodefony/testing/backplaneConformance.js +79 -3
  26. package/dist/nodefony/testing/index.js +22 -8
  27. package/dist/nodefony/testing/presenceClient.js +54 -0
  28. package/dist/types/index.d.ts +6 -1
  29. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
  30. package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
  31. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
  32. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
  33. package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
  34. package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
  35. package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
  36. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
  37. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
  38. package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
  39. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
  40. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
  41. package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
  42. package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
  43. package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
  44. package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
  45. package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
  46. package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
  47. package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
  48. package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
  49. package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
  50. package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
  51. package/dist/types/nodefony/testing/index.d.ts +1 -1
  52. package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
  53. package/docs/actions.md +1 -0
  54. package/docs/architecture.md +21 -6
  55. package/docs/canaux-a-membres.md +13 -3
  56. package/docs/cas-d-usage.md +4 -3
  57. package/docs/configuration.md +38 -2
  58. package/docs/cookbook-chat.md +400 -152
  59. package/docs/observabilite.md +28 -9
  60. package/docs/protocole.md +10 -10
  61. package/docs/securite.md +64 -10
  62. package/package.json +12 -12
@@ -33,12 +33,12 @@ var AdmissionBudget = class {
33
33
  #timedOutTotal = 0;
34
34
  #saturatedTotal = 0;
35
35
  #authenticateTimedOutTotal = 0;
36
- #windowStart = -Infinity;
36
+ #windowStart = -1e400;
37
37
  #windowTimeouts = 0;
38
38
  #openUntil = 0;
39
- #lastTimeoutWarn = -Infinity;
40
- #lastThrowWarn = -Infinity;
41
- #lastAuthenticateWarn = -Infinity;
39
+ #lastTimeoutWarn = -1e400;
40
+ #lastThrowWarn = -1e400;
41
+ #lastAuthenticateWarn = -1e400;
42
42
  /** Applique les bornes de la configuration (boot, cold path). */
43
43
  configure(limits) {
44
44
  this.authenticateTimeoutMs = limits.authenticateTimeoutMs;
@@ -159,18 +159,18 @@ var AdmissionBudget = class {
159
159
  reset() {
160
160
  this.authenticateTimeoutMs = 1e4;
161
161
  this.#authenticateTimedOutTotal = 0;
162
- this.#lastAuthenticateWarn = -Infinity;
162
+ this.#lastAuthenticateWarn = -1e400;
163
163
  this.timeoutMs = 5e3;
164
164
  this.maxPerConnection = 8;
165
165
  this.#maxTotal = 512;
166
166
  this.#pending = 0;
167
167
  this.#timedOutTotal = 0;
168
168
  this.#saturatedTotal = 0;
169
- this.#windowStart = -Infinity;
169
+ this.#windowStart = -1e400;
170
170
  this.#windowTimeouts = 0;
171
171
  this.#openUntil = 0;
172
- this.#lastTimeoutWarn = -Infinity;
173
- this.#lastThrowWarn = -Infinity;
172
+ this.#lastTimeoutWarn = -1e400;
173
+ this.#lastThrowWarn = -1e400;
174
174
  }
175
175
  };
176
176
  export { ADMISSION_GRANTED, AUTHENTICATE_TIMED_OUT, AdmissionBudget };
@@ -1,4 +1,5 @@
1
- import { declaresAction, getChannelPoliciesOf, isAdmissionClass } from "../../decorators/realtimeDecorators.js";
1
+ import { declaresAction, getDeclaredChannelPolicies, isAdmissionClass, samePolicy } from "../../decorators/realtimeDecorators.js";
2
+ import { samePresence } from "./channelPresence.js";
2
3
  import { BootConfigurationError, Injector } from "nodefony";
3
4
  // nodefony/src/server/channelPolicyRegistry.ts
4
5
  /**
@@ -14,26 +15,47 @@ import { BootConfigurationError, Injector } from "nodefony";
14
15
  * @param ctor - constructeur du contrôleur.
15
16
  * @param resolveAdmission - résolution des classes d'admission ; `null` hors
16
17
  * démarrage, et le hub refuse alors une politique qui en désigne une.
17
- * @throws BootConfigurationError Politique illisible, `allow` posé sur une
18
- * action, ou classe d'admission non résolue.
18
+ * Chaque nom est déclaré au hub SORTE PAR SORTE (lecture, écriture), sans
19
+ * héritage : le hub les fusionne avec les motifs de toutes les classes
20
+ * (ADR-0022 §6, décision 1 quater). Deux classes qui déclarent la même sorte
21
+ * différemment sous un même nom refusent le démarrage : la seconde effaçait la
22
+ * première sans un mot.
23
+ *
24
+ * @throws BootConfigurationError Politique illisible ou contredite, `allow`
25
+ * posé sur une action ou sur une écriture distincte de la lecture, classe
26
+ * d'admission non résolue, ou deux classes en désaccord sur un nom.
19
27
  */
20
28
  function registerControllerPolicies(hub, ctor, resolveAdmission) {
21
29
  if (hub.hasPolicyOwner(ctor)) return;
22
- const policies = getChannelPoliciesOf(ctor);
23
- if (policies !== null) for (const name in policies) {
24
- const policy = policies[name];
25
- if (policy === void 0) continue;
26
- const allow = policy.allow;
27
- let admission;
28
- if (allow !== void 0) {
29
- if (declaresAction(ctor, name)) throw new BootConfigurationError(`${typeof ctor === "function" ? ctor.name : "(contrôleur)"} : « allow » est posé sur l'action « ${name} » — il ne s'applique qu'aux canaux (demandé au subscribe). Une action se garde par authenticated, roles ou scopes.`);
30
- if (isAdmissionClass(allow) && resolveAdmission !== null) admission = resolveAdmission(allow, name);
30
+ const declared = getDeclaredChannelPolicies(ctor);
31
+ if (declared !== null) {
32
+ const owner = typeof ctor === "function" ? ctor.name : "(contrôleur)";
33
+ for (const [name, { read, write }] of Object.entries(declared.kinds)) {
34
+ if (write?.allow !== void 0 && read !== null && !samePolicy(read, write)) throw new BootConfigurationError(`${owner} : « allow » est posé sur l'écriture du canal « ${name} », qui déclare aussi une lecture différente — il ne s'applique qu'à la lecture (demandé au subscribe). Le poser sur @RealtimeChannel ; l'écriture se garde par authenticated, roles ou scopes.`);
35
+ const allow = (read ?? write)?.allow;
36
+ let admission;
37
+ if (allow !== void 0) {
38
+ if (declaresAction(ctor, name)) throw new BootConfigurationError(`${owner} : « allow » est posé sur l'action « ${name} » — il ne s'applique qu'aux canaux (demandé au subscribe). Une action se garde par authenticated, roles ou scopes.`);
39
+ if (isAdmissionClass(allow) && resolveAdmission !== null) admission = resolveAdmission(allow, name);
40
+ }
41
+ if (read !== null) claim(hub, name, "read", ctor, owner, read);
42
+ if (write !== null) claim(hub, name, "write", ctor, owner, write);
43
+ hub.declareChannelPolicy(name, read, write, admission);
31
44
  }
32
- hub.registerChannelPolicy(name, policy, admission);
45
+ for (const name of declared.actions) hub.declareActionDefault(name);
33
46
  }
34
47
  hub.addPolicyOwner(ctor);
35
48
  }
36
49
  /**
50
+ * Revendique une sorte de politique d'un nom pour cette classe — refuse le démarrage si une AUTRE classe en a déclaré une différente : le registre est unique par nom, une seule s'appliquerait.
51
+ */
52
+ function claim(hub, name, kind, ctor, owner, policy) {
53
+ const previous = hub.claimChannelPolicy(name, kind, ctor, policy);
54
+ if (previous === null || previous.owner === ctor || samePolicy(previous.policy, policy) && samePresence(previous.policy, policy)) return;
55
+ const which = kind === "read" ? "LECTURE" : "ÉCRITURE";
56
+ throw new BootConfigurationError(`canal temps réel « ${name} » : ${previous.ownerName} et ${owner} déclarent des politiques de ${which} différentes sous ce nom — le registre est unique par nom, une seule s'appliquerait. Les accorder, ou renommer l'un des canaux.`);
57
+ }
58
+ /**
37
59
  * 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.
38
60
  *
39
61
  * @param get - l'instance déjà construite d'une classe : le démarrage la lit
@@ -0,0 +1,191 @@
1
+ import { isAdmissionClass, samePolicy } from "../../decorators/realtimeDecorators.js";
2
+ import { BootConfigurationError, USER_CHANNEL_PREFIX, isOwnUserChannel } from "nodefony";
3
+ // nodefony/src/server/channelPolicyResolution.ts
4
+ /**
5
+ * Règle d'admission prête à appeler d'une politique, ou `null` sans `allow`.
6
+ *
7
+ * Une flèche se retient telle quelle. Une classe exige son instance — résolue au
8
+ * démarrage, jamais ici : le hub n'a pas d'injecteur, et résoudre à la première
9
+ * poignée de main laisserait sans politique un exemplaire qui n'émet que des
10
+ * webhooks. La méthode est liée une fois, et la règle se construit une fois :
11
+ * le `subscribe` n'alloue rien et n'appelle qu'une fonction.
12
+ *
13
+ * @throws BootConfigurationError La politique désigne une classe d'admission et
14
+ * aucune instance de CETTE classe n'est fournie.
15
+ */
16
+ function admissionRuleOf(name, policy, admission) {
17
+ const allow = policy.allow;
18
+ if (allow === void 0) return null;
19
+ const anonymous = policy.anonymous === true;
20
+ if (!isAdmissionClass(allow)) return {
21
+ allow,
22
+ anonymous
23
+ };
24
+ if (!(admission instanceof allow)) throw new BootConfigurationError(`canal temps réel « ${name} » : la classe d'admission ${allow.name} n'a pas été résolue au démarrage du noyau. Son contrôleur doit être déclaré par @controllers dans un module : c'est là que le framework l'inscrit et résout ses dépendances.`);
25
+ return {
26
+ allow: admission.allow.bind(admission),
27
+ anonymous
28
+ };
29
+ }
30
+ /**
31
+ * 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.
32
+ *
33
+ * @param name - nom exact ou motif (pour les messages).
34
+ * @param read - lecture déclarée, ou `null`.
35
+ * @param write - écriture déclarée, ou `null`.
36
+ * @param fallback - défaut fermé d'une action, ou `null`.
37
+ * @param admission - instance de la classe d'admission quand la politique qui
38
+ * sert de lecture en désigne une.
39
+ */
40
+ function declarationOf(name, read, write, fallback, admission) {
41
+ const policy = read ?? write ?? fallback;
42
+ return {
43
+ read,
44
+ write,
45
+ fallback,
46
+ instance: admission,
47
+ self: policy === null ? null : {
48
+ policy,
49
+ write: read !== null && write !== null && !samePolicy(read, write) ? write : null,
50
+ admission: admissionRuleOf(name, policy, admission)
51
+ }
52
+ };
53
+ }
54
+ /**
55
+ * Politique INTÉGRÉE du canal personnel `user:<id>` (ADR-0022 §6, D1) — connexion authentifiée, admise sur SON canal seulement.
56
+ *
57
+ * Constante de module : elle ne s'inscrit nulle part, donc aucun `clear()`,
58
+ * aucun ordre de démarrage, aucun oubli ne peut la retirer. Elle sert de REPLI
59
+ * de LECTURE aux noms `user:…` : seule une déclaration qui vise `user:`
60
+ * (nom exact, ou motif dont le littéral commence par `user:`) la remplace — un
61
+ * motif attrape-tout (`*`) ne retire pas la garde « son canal seulement ».
62
+ */
63
+ const USER_CHANNEL_DECLARATION = Object.freeze({
64
+ read: Object.freeze({
65
+ authenticated: true,
66
+ allow: isOwnUserChannel
67
+ }),
68
+ write: null,
69
+ fallback: null,
70
+ instance: void 0,
71
+ self: Object.freeze({
72
+ policy: Object.freeze({
73
+ authenticated: true,
74
+ allow: isOwnUserChannel
75
+ }),
76
+ write: null,
77
+ admission: Object.freeze({
78
+ allow: isOwnUserChannel,
79
+ anonymous: false
80
+ })
81
+ })
82
+ });
83
+ /**
84
+ * 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.
85
+ *
86
+ * L'ordre de spécificité : le nom exact, puis les motifs du plus spécifique au
87
+ * plus général. Pourquoi deux règles et non une :
88
+ * - la LECTURE reste à la déclaration propriétaire du nom (déclarée, sinon
89
+ * héritée de sa propre écriture) : une commande `task:start` déclarée par
90
+ * `@RealtimeInbound` sous une famille `task:*` à `allow` ne devient pas un
91
+ * canal à membres, et une écriture exacte ne voit pas sa lecture s'ouvrir par
92
+ * un motif plus général ;
93
+ * - l'ÉCRITURE descend jusqu'au motif qui en déclare une : elle s'ajoute à la
94
+ * lecture (une trame entrante exige les deux), elle ne peut que durcir — une
95
+ * écriture de famille (`mix:*` réservée) n'est plus perdue sur un nom exact
96
+ * qui ne déclare que sa lecture.
97
+ *
98
+ * Ensuite : un nom `user:…` sans déclaration qui le vise garde la politique
99
+ * intégrée du canal personnel (un motif qui ne vise pas `user:` est ignoré) ;
100
+ * une action sans politique (nom exact qui ne déclare rien) prend la lecture du
101
+ * motif qui la couvre et le défaut fermé pour écriture — un motif de lecture
102
+ * public ne l'ouvre donc pas.
103
+ *
104
+ * Rend la vue d'une déclaration quand une seule suffit (aucune allocation) ;
105
+ * n'alloue que si l'écriture vient d'une autre déclaration que la lecture.
106
+ *
107
+ * @param name - nom résolu.
108
+ * @param exact - déclaration exacte de ce nom, ou `undefined`.
109
+ * @param patterns - motifs, triés du plus spécifique au plus général, ou `null`.
110
+ * @returns les politiques, ou `null` si aucune ne couvre le nom.
111
+ */
112
+ function resolveDeclaredPolicies(name, exact, patterns) {
113
+ const declares = (d) => d.read !== null || d.write !== null;
114
+ let owner = exact !== void 0 && declares(exact) ? exact : null;
115
+ let writeFrom = exact !== void 0 && exact.write !== null ? exact : null;
116
+ const isUser = name.startsWith(USER_CHANNEL_PREFIX);
117
+ if ((owner === null || writeFrom === null) && patterns !== null) for (const pattern of patterns) {
118
+ if (isUser && !pattern.source.startsWith(USER_CHANNEL_PREFIX)) continue;
119
+ if (!pattern.re.test(name)) continue;
120
+ if (owner === null && declares(pattern)) owner = pattern;
121
+ if (writeFrom === null && pattern.write !== null) writeFrom = pattern;
122
+ if (owner !== null && writeFrom !== null) break;
123
+ }
124
+ let write = writeFrom?.write ?? null;
125
+ if (write === null && exact !== void 0 && !declares(exact)) write = exact.fallback;
126
+ let own = owner?.self ?? null;
127
+ if (own === null) {
128
+ if (isUser) own = USER_CHANNEL_DECLARATION.self;
129
+ else return exact?.self ?? null;
130
+ if (own === null) return null;
131
+ }
132
+ const distinct = write === null || samePolicy(own.policy, write) ? null : write;
133
+ if (distinct === own.write) return own;
134
+ return {
135
+ policy: own.policy,
136
+ write: distinct,
137
+ admission: own.admission
138
+ };
139
+ }
140
+ /** Échantillon d'un motif : son littéral, chaque `*` vidé — un nom qu'il couvre toujours. */
141
+ function sampleOf(pattern) {
142
+ return pattern.source.split("*").join("");
143
+ }
144
+ /**
145
+ * Deux déclarations peuvent-elles s'appliquer au MÊME nom ? Exact : le même nom, ou couvert par le motif ; deux motifs : l'un couvre l'échantillon de l'autre.
146
+ *
147
+ * Prudente : elle répond « oui » dès qu'un nom commun est plausible — elle sert à
148
+ * refuser un démarrage ambigu, jamais à ouvrir un accès.
149
+ */
150
+ function couldOverlap(a, b) {
151
+ if (a.pattern === null && b.pattern === null) return a.name === b.name;
152
+ if (a.pattern === null) return b.pattern?.re.test(a.name) === true;
153
+ if (b.pattern === null) return a.pattern.re.test(b.name);
154
+ return a.pattern.re.test(sampleOf(b.pattern)) || b.pattern.re.test(sampleOf(a.pattern));
155
+ }
156
+ /**
157
+ * 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.
158
+ *
159
+ * Cold path (inscription d'une politique, pose du verrou) : toutes les paires.
160
+ *
161
+ * @param exact - déclarations exactes, par nom.
162
+ * @param patterns - déclarations par motif.
163
+ * @param writeJudged - le verrou en place sait juger l'écriture (ou aucun verrou).
164
+ * @returns le message du refus, ou `null`.
165
+ */
166
+ function policyDeclarationsDefect(exact, patterns, writeJudged) {
167
+ const all = [];
168
+ if (exact !== null) for (const [name, decl] of exact) all.push({
169
+ name,
170
+ pattern: null,
171
+ decl
172
+ });
173
+ for (const pattern of patterns ?? []) all.push({
174
+ name: pattern.source,
175
+ pattern,
176
+ decl: pattern
177
+ });
178
+ for (const writer of all) {
179
+ const write = writer.decl.write;
180
+ if (write === null) continue;
181
+ for (const reader of all) {
182
+ const read = reader.decl.read;
183
+ if (read === null || samePolicy(read, write)) continue;
184
+ if (reader !== writer && !couldOverlap(writer, reader)) continue;
185
+ if (write.allow !== void 0) return `canal temps réel « ${writer.name} » : « allow » est posé sur l'écriture du canal « ${writer.name} », qui rencontre une lecture différente (« ${reader.name} ») — il ne s'applique qu'à la lecture (demandé au subscribe). Le poser sur @RealtimeChannel ; l'écriture se garde par authenticated, roles ou scopes.`;
186
+ if (!writeJudged) return `canal temps réel « ${writer.name} » : il déclare une politique d'ÉCRITURE distincte de sa lecture, et le verrou des trames posé sur le hub ne sait pas la juger (il ne déclare pas « judgesWrite ») — l'écriture resterait ouverte. Aligner @nodefony/security sur la version de @nodefony/realtime.`;
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+ export { USER_CHANNEL_DECLARATION, admissionRuleOf, declarationOf, policyDeclarationsDefect, resolveDeclaredPolicies };