@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.
- package/README.md +55 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +91 -12
- package/dist/nodefony/config/config.js +7 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +341 -43
- package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
- package/dist/nodefony/src/backplane/RedisBackplane.js +146 -41
- package/dist/nodefony/src/backplane/controlOrder.js +86 -0
- package/dist/nodefony/src/backplane/envelope.js +505 -27
- package/dist/nodefony/src/backplane/ingress.js +93 -0
- package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
- package/dist/nodefony/src/backplane/publishQueue.js +97 -3
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
- package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
- package/dist/nodefony/src/server/RealtimeConnection.js +135 -0
- package/dist/nodefony/src/server/RealtimeController.js +409 -107
- package/dist/nodefony/src/server/RealtimeHub.js +1158 -111
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
- package/dist/nodefony/src/server/admissionBudget.js +176 -0
- package/dist/nodefony/src/server/channelPolicyRegistry.js +95 -0
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/connectionAdmission.js +242 -0
- package/dist/nodefony/src/server/contractRoutes.js +75 -0
- package/dist/nodefony/src/server/deniedDetail.js +10 -3
- package/dist/nodefony/src/server/frameAdmission.js +160 -0
- package/dist/nodefony/src/server/presenceBus.js +486 -0
- package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
- package/dist/nodefony/src/server/remotePresence.js +541 -0
- package/dist/nodefony/src/server/subscribedAck.js +13 -1
- package/dist/nodefony/src/server/syslogUplink.js +3 -4
- package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
- package/dist/nodefony/src/service/RealtimeService.js +51 -1
- package/dist/nodefony/src/service/publicOrigin.js +40 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
- package/dist/nodefony/testing/backplaneConformance.js +347 -0
- package/dist/nodefony/testing/index.js +43 -12
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +39 -7
- package/dist/types/nodefony/config/config.d.ts +9 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +198 -17
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +46 -51
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
- package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +89 -18
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +75 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +77 -0
- package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
- package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
- package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +20 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +110 -12
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +448 -42
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
- package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +41 -0
- package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
- package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
- package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +66 -0
- package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
- package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
- package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +10 -0
- package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +17 -2
- package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +13 -2
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +420 -18
- package/docs/architecture.md +101 -11
- package/docs/cadence-et-contre-pression.md +87 -11
- package/docs/canaux-a-membres.md +277 -0
- package/docs/cas-d-usage.md +1703 -0
- package/docs/configuration.md +175 -19
- package/docs/cookbook-chat.md +701 -224
- package/docs/index.md +11 -1
- package/docs/observabilite.md +97 -21
- package/docs/protocole.md +142 -33
- package/docs/securite.md +347 -41
- package/docs/vocabulaire.md +13 -12
- package/package.json +16 -13
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isCallable, isRecord } from "../src/guards.js";
|
|
2
|
+
import { presenceDefect, samePresence } from "../src/server/channelPresence.js";
|
|
3
|
+
import { BootConfigurationError, Injector, channelPatternWeight, compileChannelPattern } from "nodefony";
|
|
2
4
|
import "reflect-metadata";
|
|
3
5
|
// nodefony/decorators/realtimeDecorators.ts
|
|
4
6
|
/** Clés de métadonnées posées sur le constructor de la classe controller. */
|
|
@@ -6,6 +8,10 @@ const ACTIONS_KEY = "realtime:actions";
|
|
|
6
8
|
const CHANNELS_KEY = "realtime:channels";
|
|
7
9
|
const INBOUND_KEY = "realtime:inbound";
|
|
8
10
|
const POLICIES_KEY = "realtime:policies";
|
|
11
|
+
const WRITE_POLICIES_KEY = "realtime:write-policies";
|
|
12
|
+
const OWN_READ_KEY = "realtime:policies:own-read";
|
|
13
|
+
const OWN_WRITE_KEY = "realtime:policies:own-write";
|
|
14
|
+
const CONFLICTS_KEY = "realtime:policies:conflicts";
|
|
9
15
|
/**
|
|
10
16
|
* Lit une métadonnée de classe posée par ces décorateurs : un objet nom → valeur,
|
|
11
17
|
* ou `undefined` quand la classe n'en porte pas. Les valeurs restent `unknown` :
|
|
@@ -16,33 +22,138 @@ function readMetadataMap(key, ctor) {
|
|
|
16
22
|
const raw = Reflect.getMetadata(key, ctor);
|
|
17
23
|
return isRecord(raw) ? raw : void 0;
|
|
18
24
|
}
|
|
25
|
+
/** Comme {@link readMetadataMap}, sans remonter la chaîne de prototypes : ce que la classe a posé ELLE-MÊME. */
|
|
26
|
+
function readOwnMetadataMap(key, ctor) {
|
|
27
|
+
const raw = Reflect.getOwnMetadata(key, ctor);
|
|
28
|
+
return isRecord(raw) ? raw : void 0;
|
|
29
|
+
}
|
|
19
30
|
/** Vrai si la valeur est un tableau de chaînes. */
|
|
20
31
|
function isStringArray(value) {
|
|
21
32
|
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
22
33
|
}
|
|
23
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Clés qu'une politique de canal peut porter — toute autre est refusée : une
|
|
36
|
+
* faute de frappe (`role`, `alow`) dans une règle de sécurité ne s'ignore pas.
|
|
37
|
+
*/
|
|
38
|
+
const POLICY_KEYS = /* @__PURE__ */ new Set([
|
|
39
|
+
"authenticated",
|
|
40
|
+
"roles",
|
|
41
|
+
"scopes",
|
|
42
|
+
"allow",
|
|
43
|
+
"anonymous",
|
|
44
|
+
"presence"
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* `allow` désigne-t-il une classe d'admission (`@injectable`) plutôt qu'une flèche ?
|
|
48
|
+
*
|
|
49
|
+
* Tranché par la métadonnée `@injectable`, jamais par le texte de la fonction
|
|
50
|
+
* (`class …`), qu'un minifieur réécrit (ADR-0021 §S10 bis).
|
|
51
|
+
*
|
|
52
|
+
* @param allow - valeur du membre `allow` d'une politique déjà validée.
|
|
53
|
+
* @returns `true` pour une classe à résoudre par l'injecteur.
|
|
54
|
+
*/
|
|
55
|
+
function isAdmissionClass(allow) {
|
|
56
|
+
return Injector.isInjectable(allow);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Défaut d'une politique de canal, ou `null` si elle est recevable — en clair, pour que le refus au démarrage dise QUOI corriger.
|
|
60
|
+
*
|
|
61
|
+
* @param value - valeur lue sous la métadonnée des politiques (`unknown` :
|
|
62
|
+
* `Reflect.defineMetadata` est public).
|
|
63
|
+
* @returns la raison du refus, ou `null`.
|
|
64
|
+
*/
|
|
65
|
+
function channelPolicyDefect(value) {
|
|
66
|
+
if (!isRecord(value)) return "la politique n'est pas un objet";
|
|
67
|
+
for (const key of Object.keys(value)) if (!POLICY_KEYS.has(key)) return `clé inconnue « ${key} » (clés admises : ${[...POLICY_KEYS].join(", ")})`;
|
|
68
|
+
const { authenticated, roles, scopes, allow, anonymous, presence } = value;
|
|
69
|
+
const presenceIssue = presenceDefect(presence);
|
|
70
|
+
if (presenceIssue !== null) return presenceIssue;
|
|
71
|
+
if (authenticated !== void 0 && typeof authenticated !== "boolean") return "« authenticated » doit être un booléen";
|
|
72
|
+
if (roles !== void 0 && !isStringArray(roles)) return "« roles » doit être un tableau de chaînes";
|
|
73
|
+
if (scopes !== void 0 && !isStringArray(scopes)) return "« scopes » doit être un tableau de chaînes";
|
|
74
|
+
if (anonymous !== void 0 && typeof anonymous !== "boolean") return "« anonymous » doit être un booléen";
|
|
75
|
+
if (allow === void 0) return anonymous === void 0 ? null : "« anonymous » n'a de sens qu'avec « allow »";
|
|
76
|
+
if (typeof allow !== "function") return "« allow » doit être une fonction ou une classe @injectable";
|
|
77
|
+
if (anonymous === true && authenticated === true) return "« anonymous: true » contredit « authenticated: true »";
|
|
78
|
+
const proto = Reflect.get(allow, "prototype");
|
|
79
|
+
if (!Injector.isInjectable(allow) && isRecord(proto) && typeof proto.allow === "function") return `« allow » est une classe (${allow.name}) sans @injectable`;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/** Une valeur lue sous la métadonnée des politiques est-elle une politique recevable ? */
|
|
24
83
|
function isChannelPolicy(value) {
|
|
25
|
-
|
|
26
|
-
const { authenticated, roles, scopes } = value;
|
|
27
|
-
return (authenticated === void 0 || typeof authenticated === "boolean") && (roles === void 0 || isStringArray(roles)) && (scopes === void 0 || isStringArray(scopes));
|
|
84
|
+
return channelPolicyDefect(value) === null;
|
|
28
85
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
86
|
+
const NO_NAMES = [];
|
|
87
|
+
/** Deux listes de rôles ou de scopes désignent-elles le même ENSEMBLE — ordre et doublons indifférents, absente = vide ? */
|
|
88
|
+
function sameNames(a, b) {
|
|
89
|
+
const x = a ?? NO_NAMES;
|
|
90
|
+
const y = b ?? NO_NAMES;
|
|
91
|
+
for (const name of x) if (!y.includes(name)) return false;
|
|
92
|
+
for (const name of y) if (!x.includes(name)) return false;
|
|
32
93
|
return true;
|
|
33
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Deux politiques exigent-elles exactement la même chose ? Structurel : deux littéraux identiques écrits dans deux classes le sont ; `allow` se compare par référence.
|
|
97
|
+
*
|
|
98
|
+
* Sert à refuser deux déclarations CONTRADICTOIRES d'un même nom (une seule
|
|
99
|
+
* s'appliquerait) sans refuser deux déclarations ÉQUIVALENTES, et à ne juger
|
|
100
|
+
* l'écriture que si elle diffère de la lecture. Démarrage seulement.
|
|
101
|
+
*
|
|
102
|
+
* @param a - une politique recevable.
|
|
103
|
+
* @param b - une politique recevable.
|
|
104
|
+
* @returns `true` si aucun pair ne peut satisfaire l'une sans l'autre.
|
|
105
|
+
*/
|
|
106
|
+
function samePolicy(a, b) {
|
|
107
|
+
if (a === b) return true;
|
|
108
|
+
return a.authenticated === true === (b.authenticated === true) && sameNames(a.roles, b.roles) && sameNames(a.scopes, b.scopes) && a.allow === b.allow && a.anonymous === true === (b.anonymous === true);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Exige que chaque entrée du registre soit une politique recevable — sinon le démarrage s'arrête en nommant la classe, le canal et le défaut.
|
|
112
|
+
*
|
|
113
|
+
* Une politique illisible ne se laisse jamais tomber : sans elle, le canal
|
|
114
|
+
* s'ouvrirait à qui la politique fermait (fail-closed).
|
|
115
|
+
*/
|
|
116
|
+
function assertPolicyMap(owner, map) {
|
|
117
|
+
for (const name in map) {
|
|
118
|
+
const defect = channelPolicyDefect(map[name]);
|
|
119
|
+
if (defect !== null) throw new BootConfigurationError(`${owner} : politique du canal temps réel « ${name} » refusée — ${defect}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
34
122
|
/** Méthode de l'instance que désigne une entrée de registre, ou `undefined`. */
|
|
35
123
|
function methodOf(instance, prop) {
|
|
36
124
|
return typeof prop === "string" || typeof prop === "symbol" ? Reflect.get(instance, prop) : void 0;
|
|
37
125
|
}
|
|
126
|
+
/** La politique porte-t-elle au moins une clé propre non `undefined` ? */
|
|
127
|
+
function hasDefinedKey(policy) {
|
|
128
|
+
for (const key of Object.keys(policy)) if (Reflect.get(policy, key) !== void 0) return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
38
131
|
/** Libération d'un canal dont la fabrique décorée n'en a rendu aucune. */
|
|
39
132
|
const NO_DISPOSE = () => {};
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Déclare une politique non vide d'une sorte sous son nom — sans jamais écraser une déclaration contradictoire de la même classe : celle-ci est retenue pour refuser le démarrage.
|
|
135
|
+
*/
|
|
136
|
+
function declarePolicy(ctor, name, kind, policy) {
|
|
137
|
+
if (!policy || !hasDefinedKey(policy)) return;
|
|
138
|
+
const key = kind === "read" ? POLICIES_KEY : WRITE_POLICIES_KEY;
|
|
139
|
+
const ownKey = kind === "read" ? OWN_READ_KEY : OWN_WRITE_KEY;
|
|
140
|
+
const own = readOwnMetadataMap(ownKey, ctor);
|
|
141
|
+
const current = readMetadataMap(key, ctor)?.[name];
|
|
142
|
+
if (own?.[name] === true && current !== void 0 && !(isChannelPolicy(current) && isChannelPolicy(policy) && samePolicy(current, policy) && samePresence(current, policy))) {
|
|
143
|
+
Reflect.defineMetadata(CONFLICTS_KEY, {
|
|
144
|
+
...readOwnMetadataMap(CONFLICTS_KEY, ctor),
|
|
145
|
+
[name]: kind
|
|
146
|
+
}, ctor);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
Reflect.defineMetadata(key, {
|
|
150
|
+
...readMetadataMap(key, ctor),
|
|
151
|
+
[name]: policy
|
|
152
|
+
}, ctor);
|
|
153
|
+
Reflect.defineMetadata(ownKey, {
|
|
154
|
+
...own,
|
|
155
|
+
[name]: true
|
|
156
|
+
}, ctor);
|
|
46
157
|
}
|
|
47
158
|
/**
|
|
48
159
|
* Politique appliquée à une action RPC dont l'auteur n'a rien déclaré :
|
|
@@ -51,11 +162,6 @@ function definePolicy(ctor, name, policy) {
|
|
|
51
162
|
* Pour une action délibérément publique, l'écrire : `{ authenticated: false }`.
|
|
52
163
|
*/
|
|
53
164
|
const DEFAULT_ACTION_POLICY = Object.freeze({ authenticated: true });
|
|
54
|
-
/** Une politique est-elle DÉJÀ déclarée sous ce nom ? */
|
|
55
|
-
function hasPolicy(ctor, name) {
|
|
56
|
-
const map = readMetadataMap(POLICIES_KEY, ctor);
|
|
57
|
-
return map ? Object.prototype.hasOwnProperty.call(map, name) : false;
|
|
58
|
-
}
|
|
59
165
|
/**
|
|
60
166
|
* Décorateur **méthode** — expose une **action RPC** request→response du
|
|
61
167
|
* controller realtime sous le nom `method` (ex. `"nodefony:kernel:ping"`, `"chat:send"`).
|
|
@@ -75,9 +181,12 @@ function hasPolicy(ctor, name) {
|
|
|
75
181
|
* défaut, toute action applicative serait publique sans que rien ne le dise.
|
|
76
182
|
* Pour une action délibérément ouverte, l'écrire : `{ authenticated: false }`.
|
|
77
183
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
184
|
+
* Une politique d'action est une politique d'**écriture** : l'appel exige aussi
|
|
185
|
+
* la lecture déclarée sous le même nom. Sans politique, l'action **hérite** de la
|
|
186
|
+
* lecture d'un canal homonyme (`@RealtimeChannel`) si elle existe — le défaut ne
|
|
187
|
+
* rétrograde jamais une politique déclarée —, et sinon du défaut fermé, quel que
|
|
188
|
+
* soit l'ordre des méthodes. Deux politiques explicites différentes sur le même
|
|
189
|
+
* nom (action et `@RealtimeInbound`) refusent le démarrage.
|
|
81
190
|
*
|
|
82
191
|
* ⚠️ Le NOM peut primer sur ta politique : le plancher système de
|
|
83
192
|
* `@nodefony/security` réserve les préfixes plateforme (`kernel:`, `syslog:`…)
|
|
@@ -107,21 +216,62 @@ function RealtimeAction(method, policy) {
|
|
|
107
216
|
const map = { ...readMetadataMap(ACTIONS_KEY, ctor) };
|
|
108
217
|
map[method] = propertyKey;
|
|
109
218
|
Reflect.defineMetadata(ACTIONS_KEY, map, ctor);
|
|
110
|
-
|
|
111
|
-
|
|
219
|
+
declarePolicy(ctor, method, "write", policy);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Décorateur de **classe** — pose les politiques d'accès d'actions ou de canaux
|
|
224
|
+
* par leur NOM, sans déclarer de handler.
|
|
225
|
+
*
|
|
226
|
+
* C'est la voie des actions servies par la table de `realtimeActions()` : cette
|
|
227
|
+
* table ne sait pas porter de politique, et un `@RealtimeAction` ajouté pour la
|
|
228
|
+
* seule politique ferait fabriquer, à chaque connexion, un handler aussitôt
|
|
229
|
+
* écrasé par la table (et contournerait l'injection de ses paramètres décorés).
|
|
230
|
+
* Ici, rien par connexion : les politiques sont écrites UNE fois, à la
|
|
231
|
+
* définition de la classe, dans le même registre que celles des décorateurs de
|
|
232
|
+
* méthode. Une action de la table sans politique garde le défaut FERMÉ
|
|
233
|
+
* (connexion authentifiée).
|
|
234
|
+
*
|
|
235
|
+
* Un nom qui porte `*` est un motif (famille de canaux), comme partout ailleurs.
|
|
236
|
+
*
|
|
237
|
+
* La politique vaut pour la LECTURE et l'ÉCRITURE du nom (le même objet, jugé
|
|
238
|
+
* une fois). Elle prime sur le défaut fermé d'une action ; contredire une
|
|
239
|
+
* politique explicite d'un décorateur de méthode refuse le démarrage.
|
|
240
|
+
*
|
|
241
|
+
* **Sur un contrôleur de contrat** (`extends ContractRealtimeController<C>`), le
|
|
242
|
+
* décorateur lit le contrat sur la classe qu'il décore : chaque nom doit être
|
|
243
|
+
* une méthode ou un canal de `C`, sinon la classe ne compile pas, et l'erreur
|
|
244
|
+
* nomme le fautif (`notInContract`). Le contrat ne se répète pas. Sur un
|
|
245
|
+
* contrôleur sans contrat, tout nom est accepté. Vérification à la compilation
|
|
246
|
+
* seulement : coût d'exécution nul.
|
|
247
|
+
*
|
|
248
|
+
* @param policies - nom d'action ou de canal → politique.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* @RealtimePolicy({ "orders:quote": { roles: ["ROLE_SALES"] } })
|
|
252
|
+
* class OrdersRt extends ContractRealtimeController<OrdersContract> { … }
|
|
253
|
+
*/
|
|
254
|
+
function RealtimePolicy(policies) {
|
|
255
|
+
return function(target) {
|
|
256
|
+
for (const [name, policy] of Object.entries(policies)) {
|
|
257
|
+
declarePolicy(target, name, "read", policy);
|
|
258
|
+
declarePolicy(target, name, "write", policy);
|
|
259
|
+
}
|
|
112
260
|
};
|
|
113
261
|
}
|
|
114
262
|
/**
|
|
115
|
-
* Décorateur **méthode** — expose un **canal pub/sub** sous le nom `channel
|
|
116
|
-
*
|
|
117
|
-
* un `dispose: () => void` (la base le mémorise et l'appelle au dernier
|
|
263
|
+
* Décorateur **méthode** — expose un **canal pub/sub** sous le nom `channel`, ou
|
|
264
|
+
* une FAMILLE de canaux sous un motif (`chat:room:*`). La méthode décorée DOIT
|
|
265
|
+
* renvoyer un `dispose: () => void` (la base le mémorise et l'appelle au dernier
|
|
118
266
|
* désabonné, comme avec l'override classique).
|
|
119
267
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
268
|
+
* Un motif (`*` = toute suite de caractères, la grammaire des politiques) sert
|
|
269
|
+
* chaque nom qu'il couvre : `@RealtimeChannel("chat:room:*", { allow })` déclare
|
|
270
|
+
* la règle ET la fabrique de tous les salons, et la méthode reçoit le nom réel
|
|
271
|
+
* (`chat:room:42`) en premier argument. Ordre de résolution au `subscribe` : le
|
|
272
|
+
* nom exact décoré, puis l'override `createRealtimeChannel()` (canaux à suffixe
|
|
273
|
+
* de granularité, regex — un contrôleur qui en a un garde son comportement),
|
|
274
|
+
* puis les motifs décorés, du plus spécifique au plus général.
|
|
125
275
|
*
|
|
126
276
|
* Le `this` est lié à l'instance ; le factory peut donc lire `this.kernel`, etc.
|
|
127
277
|
* Mais ATTENTION : un provider est **partagé** entre toutes les connexions au
|
|
@@ -129,8 +279,20 @@ function RealtimeAction(method, policy) {
|
|
|
129
279
|
* créé. Capturer des **valeurs long-lived** (broker, syslog) dans la closure,
|
|
130
280
|
* jamais `this.context` (lié à la connexion créatrice qui peut fermer).
|
|
131
281
|
*
|
|
282
|
+
* La `policy` est celle de **LECTURE** : exigée au `subscribe`, et en plus de
|
|
283
|
+
* l'écriture sur toute trame entrante du même nom. Absente, la lecture hérite de
|
|
284
|
+
* l'écriture (`@RealtimeInbound`) si elle est déclarée — une application qui ne
|
|
285
|
+
* garde que l'écriture reste protégée en lecture. Pour un canal que tout le
|
|
286
|
+
* monde lit et que seuls certains écrivent, l'écrire : `{ authenticated: false }`.
|
|
287
|
+
*
|
|
132
288
|
* @example
|
|
133
289
|
* class ChatController extends RealtimeController {
|
|
290
|
+
* // Tous les salons : la règle et la fabrique, une seule fois.
|
|
291
|
+
* @RealtimeChannel("chat:room:*", { allow: RoomAdmission })
|
|
292
|
+
* room(_channel: string): () => void {
|
|
293
|
+
* return () => {};
|
|
294
|
+
* }
|
|
295
|
+
*
|
|
134
296
|
* @RealtimeChannel("chat:room42")
|
|
135
297
|
* room42(_channel: string, publish: RealtimePublish): () => void {
|
|
136
298
|
* const timer = setInterval(() => publish("chat:room42", { ts: Date.now() }), 1000);
|
|
@@ -144,7 +306,7 @@ function RealtimeChannel(channel, policy) {
|
|
|
144
306
|
const map = { ...readMetadataMap(CHANNELS_KEY, ctor) };
|
|
145
307
|
map[channel] = propertyKey;
|
|
146
308
|
Reflect.defineMetadata(CHANNELS_KEY, map, ctor);
|
|
147
|
-
|
|
309
|
+
declarePolicy(ctor, channel, "read", policy);
|
|
148
310
|
};
|
|
149
311
|
}
|
|
150
312
|
/**
|
|
@@ -159,6 +321,20 @@ function RealtimeChannel(channel, policy) {
|
|
|
159
321
|
* pousser tant qu'aucun handler n'est enregistré ici). Cf seam P13.8a
|
|
160
322
|
* (`beforeDispatch`) pour la gate sécurité par voter P6.
|
|
161
323
|
*
|
|
324
|
+
* La `policy` est celle d'**ÉCRITURE** : une trame entrante exige la LECTURE du
|
|
325
|
+
* même nom (`@RealtimeChannel`) ET elle — l'écriture n'est jamais moins stricte
|
|
326
|
+
* que la lecture. Sans lecture déclarée, la lecture en hérite.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* class ChatController extends RealtimeController {
|
|
330
|
+
* @RealtimeChannel("chat:typing", { authenticated: false }) // tout le monde lit
|
|
331
|
+
* typing(_channel: string, _publish: RealtimePublish): () => void {
|
|
332
|
+
* return () => {};
|
|
333
|
+
* }
|
|
334
|
+
* @RealtimeInbound("chat:typing", { authenticated: true }) // seuls les connectés écrivent
|
|
335
|
+
* onTyping(): void {}
|
|
336
|
+
* }
|
|
337
|
+
*
|
|
162
338
|
* @example
|
|
163
339
|
* class ChatController extends RealtimeController {
|
|
164
340
|
* @RealtimeInbound("chat:send")
|
|
@@ -175,7 +351,7 @@ function RealtimeInbound(method, policy) {
|
|
|
175
351
|
const map = { ...readMetadataMap(INBOUND_KEY, ctor) };
|
|
176
352
|
map[method] = propertyKey;
|
|
177
353
|
Reflect.defineMetadata(INBOUND_KEY, map, ctor);
|
|
178
|
-
|
|
354
|
+
declarePolicy(ctor, method, "write", policy);
|
|
179
355
|
};
|
|
180
356
|
}
|
|
181
357
|
/**
|
|
@@ -216,6 +392,47 @@ function getRealtimeChannels(instance) {
|
|
|
216
392
|
}
|
|
217
393
|
return out;
|
|
218
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Motifs décorés de chaque classe, compilés au premier besoin : du plus
|
|
397
|
+
* spécifique au plus général. Par CLASSE et non par instance — un contrôleur
|
|
398
|
+
* temps réel est instancié par connexion, ses motifs ne changent pas.
|
|
399
|
+
*/
|
|
400
|
+
const decoratedPatterns = /* @__PURE__ */ new WeakMap();
|
|
401
|
+
/**
|
|
402
|
+
* La fabrique décorée sous un MOTIF qui couvre `channel` — `@RealtimeChannel("chat:room:*")` sert `chat:room:42` —, ou `null` si aucun motif ne le couvre.
|
|
403
|
+
*
|
|
404
|
+
* Même grammaire que les politiques (`compileChannelPattern`) et même
|
|
405
|
+
* départage (`channelPatternWeight`, le plus spécifique d'abord) : un nom
|
|
406
|
+
* reçoit la fabrique du motif dont il reçoit la règle. Un motif refusé par la
|
|
407
|
+
* grammaire (trop d'étoiles) ne sert rien — le hub le dit déjà au démarrage.
|
|
408
|
+
* Cold path : appelé au premier abonné d'un canal qu'aucun nom exact ni
|
|
409
|
+
* `createRealtimeChannel` ne produit.
|
|
410
|
+
*
|
|
411
|
+
* @param instance - le contrôleur ; ses motifs se lisent sur sa classe.
|
|
412
|
+
* @param channels - le registre rendu par {@link getRealtimeChannels}.
|
|
413
|
+
* @param channel - le nom demandé.
|
|
414
|
+
*/
|
|
415
|
+
function patternChannelFactory(instance, channels, channel) {
|
|
416
|
+
if (channels === null) return null;
|
|
417
|
+
let patterns = decoratedPatterns.get(instance.constructor);
|
|
418
|
+
if (patterns === void 0) {
|
|
419
|
+
const compiled = [];
|
|
420
|
+
for (const source of Object.keys(channels)) {
|
|
421
|
+
if (!source.includes("*")) continue;
|
|
422
|
+
const re = compileChannelPattern(source);
|
|
423
|
+
if (re !== null) compiled.push({
|
|
424
|
+
source,
|
|
425
|
+
re,
|
|
426
|
+
weight: channelPatternWeight(source)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
compiled.sort((a, b) => b.weight - a.weight);
|
|
430
|
+
patterns = compiled;
|
|
431
|
+
decoratedPatterns.set(instance.constructor, patterns);
|
|
432
|
+
}
|
|
433
|
+
for (const pattern of patterns) if (pattern.re.test(channel)) return channels[pattern.source] ?? null;
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
219
436
|
/** Lecture (lazy, au handshake) du registre des canaux inbound décorés. */
|
|
220
437
|
function getRealtimeInbound(instance) {
|
|
221
438
|
const map = readMetadataMap(INBOUND_KEY, instance.constructor);
|
|
@@ -228,17 +445,98 @@ function getRealtimeInbound(instance) {
|
|
|
228
445
|
return out;
|
|
229
446
|
}
|
|
230
447
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
|
|
448
|
+
* La classe déclare-t-elle une ACTION (`@RealtimeAction`) sous ce nom ? Sert à refuser un `allow` posé sur une action, que rien n'appliquerait.
|
|
449
|
+
*
|
|
450
|
+
* @param ctor - constructeur du contrôleur.
|
|
451
|
+
* @param name - nom d'action ou de canal.
|
|
452
|
+
*/
|
|
453
|
+
function declaresAction(ctor, name) {
|
|
454
|
+
const map = readMetadataMap(ACTIONS_KEY, ctor);
|
|
455
|
+
return map !== void 0 && Object.prototype.hasOwnProperty.call(map, name);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Politiques déclarées sur une CLASSE de contrôleur, validées et résolues par sorte — lues au démarrage du noyau, jamais par trame.
|
|
459
|
+
*
|
|
460
|
+
* Résolution indépendante de l'ordre des méthodes :
|
|
461
|
+
* - l'écriture d'un nom est celle déclarée (`@RealtimeInbound`, `@RealtimeAction`
|
|
462
|
+
* avec politique, `@RealtimePolicy`) ; pour une action SANS politique, le
|
|
463
|
+
* défaut fermé, sauf si une lecture est déclarée — l'action en hérite alors ;
|
|
464
|
+
* - la lecture est celle déclarée (`@RealtimeChannel`, `@RealtimePolicy`),
|
|
465
|
+
* sinon l'écriture : une application qui ne garde que l'écriture reste
|
|
466
|
+
* protégée en lecture ;
|
|
467
|
+
* - `kinds` garde ce que la classe déclare, SANS héritage : le hub le fusionne
|
|
468
|
+
* avec les motifs de toutes les classes, sorte par sorte.
|
|
469
|
+
*
|
|
470
|
+
* @param ctor - constructeur du contrôleur.
|
|
471
|
+
* @returns les deux tables, ou `null` si la classe ne déclare aucune politique.
|
|
472
|
+
* @throws BootConfigurationError Une politique est illisible, ou deux
|
|
473
|
+
* déclarations d'une même sorte se contredisent : la classe, le canal et le
|
|
474
|
+
* défaut sont nommés.
|
|
475
|
+
*/
|
|
476
|
+
function getDeclaredChannelPolicies(ctor) {
|
|
477
|
+
const readMap = readMetadataMap(POLICIES_KEY, ctor);
|
|
478
|
+
const writeMap = readMetadataMap(WRITE_POLICIES_KEY, ctor);
|
|
479
|
+
const actions = readMetadataMap(ACTIONS_KEY, ctor);
|
|
480
|
+
if (readMap === void 0 && writeMap === void 0 && actions === void 0) return null;
|
|
481
|
+
const owner = typeof ctor === "function" ? ctor.name : "(contrôleur)";
|
|
482
|
+
const conflicts = readMetadataMap(CONFLICTS_KEY, ctor);
|
|
483
|
+
if (conflicts !== void 0) for (const [name, kind] of Object.entries(conflicts)) throw new BootConfigurationError(`${owner} : le canal temps réel « ${name} » déclare deux politiques de ${kind === "read" ? "LECTURE" : "ÉCRITURE"} différentes — une seule s'appliquerait, l'autre disparaîtrait sans un mot. Garder une seule déclaration ; pour garder la lecture et l'écriture séparément : @RealtimeChannel(nom, lecture) et @RealtimeInbound(nom, écriture).`);
|
|
484
|
+
if (readMap !== void 0) assertPolicyMap(owner, readMap);
|
|
485
|
+
if (writeMap !== void 0) assertPolicyMap(owner, writeMap);
|
|
486
|
+
const read = {};
|
|
487
|
+
const kinds = {};
|
|
488
|
+
let any = false;
|
|
489
|
+
if (readMap !== void 0) for (const [name, policy] of Object.entries(readMap)) {
|
|
490
|
+
read[name] = policy;
|
|
491
|
+
kinds[name] = {
|
|
492
|
+
read: policy,
|
|
493
|
+
write: null
|
|
494
|
+
};
|
|
495
|
+
any = true;
|
|
496
|
+
}
|
|
497
|
+
if (writeMap !== void 0) for (const [name, policy] of Object.entries(writeMap)) {
|
|
498
|
+
kinds[name] = {
|
|
499
|
+
read: kinds[name]?.read ?? null,
|
|
500
|
+
write: policy
|
|
501
|
+
};
|
|
502
|
+
read[name] ??= policy;
|
|
503
|
+
any = true;
|
|
504
|
+
}
|
|
505
|
+
const actionNames = actions === void 0 ? [] : Object.keys(actions);
|
|
506
|
+
for (const name of actionNames) {
|
|
507
|
+
if (Object.hasOwn(read, name)) continue;
|
|
508
|
+
read[name] = DEFAULT_ACTION_POLICY;
|
|
509
|
+
any = true;
|
|
510
|
+
}
|
|
511
|
+
return any ? {
|
|
512
|
+
read,
|
|
513
|
+
kinds,
|
|
514
|
+
actions: actionNames
|
|
515
|
+
} : null;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Politiques de LECTURE déclarées sur une CLASSE de contrôleur (`@RealtimeChannel`/`@RealtimeInbound`/`@RealtimeAction`/`@RealtimePolicy`), validées — lues au démarrage du noyau, jamais par trame.
|
|
519
|
+
*
|
|
520
|
+
* La lecture d'un nom est celle exigée au `subscribe` ; une écriture distincte
|
|
521
|
+
* se lit par {@link getDeclaredChannelPolicies}.
|
|
522
|
+
*
|
|
523
|
+
* @param ctor - constructeur du contrôleur.
|
|
524
|
+
* @returns nom de canal ou de méthode → politique de lecture, ou `null` si la
|
|
525
|
+
* classe n'en déclare aucune.
|
|
526
|
+
* @throws BootConfigurationError Une politique est illisible ou contredite :
|
|
527
|
+
* la classe, le canal et le défaut sont nommés.
|
|
528
|
+
*/
|
|
529
|
+
function getChannelPoliciesOf(ctor) {
|
|
530
|
+
return getDeclaredChannelPolicies(ctor)?.read ?? null;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Politiques déclarées sur la classe d'une instance de contrôleur — même lecture que {@link getChannelPoliciesOf}.
|
|
534
|
+
*
|
|
535
|
+
* @param instance - contrôleur instancié.
|
|
536
|
+
* @returns nom → politique, ou `null` si aucune.
|
|
236
537
|
*/
|
|
237
538
|
function getRealtimeChannelPolicies(instance) {
|
|
238
|
-
|
|
239
|
-
if (map === void 0) return null;
|
|
240
|
-
if (!isPolicyMap(map)) throw new TypeError(`${instance.constructor.name} : politique de canal temps réel illisible`);
|
|
241
|
-
return map;
|
|
539
|
+
return getChannelPoliciesOf(instance.constructor);
|
|
242
540
|
}
|
|
243
541
|
/**
|
|
244
542
|
* Préfixes de canaux **broadcast** déclarés statiquement, tous controllers
|
|
@@ -287,4 +585,4 @@ function getDeclaredBroadcastPrefixes() {
|
|
|
287
585
|
function applyDeclaredBroadcastPrefixes(hub) {
|
|
288
586
|
for (const prefix of declaredBroadcastPrefixes) hub.markBroadcastChannel(prefix);
|
|
289
587
|
}
|
|
290
|
-
export { DEFAULT_ACTION_POLICY, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, applyDeclaredBroadcastPrefixes, getDeclaredBroadcastPrefixes, getRealtimeActions, getRealtimeChannelPolicies, getRealtimeChannels, getRealtimeInbound };
|
|
588
|
+
export { DEFAULT_ACTION_POLICY, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, RealtimePolicy, applyDeclaredBroadcastPrefixes, channelPolicyDefect, declaresAction, getChannelPoliciesOf, getDeclaredBroadcastPrefixes, getDeclaredChannelPolicies, getRealtimeActions, getRealtimeChannelPolicies, getRealtimeChannels, getRealtimeInbound, isAdmissionClass, patternChannelFactory, samePolicy };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import resolveBackplaneOriginId from "./originId.js";
|
|
2
|
-
import { CLUSTER_RT_KIND } from "nodefony";
|
|
2
|
+
import { CLUSTER_IPC_ADVANCED, CLUSTER_IPC_SERIALIZATION_ENV, CLUSTER_RT_KIND } from "nodefony";
|
|
3
3
|
// nodefony/src/backplane/ClusterBackplane.ts
|
|
4
4
|
/**
|
|
5
5
|
* Transport de production : `process.send` (émission) + `process.on("message")`
|
|
@@ -13,6 +13,9 @@ const processIpcTransport = {
|
|
|
13
13
|
},
|
|
14
14
|
onReceive(cb) {
|
|
15
15
|
process.on("message", (msg) => cb(msg));
|
|
16
|
+
},
|
|
17
|
+
carriesBytes() {
|
|
18
|
+
return process.channel !== void 0 && process.env[CLUSTER_IPC_SERIALIZATION_ENV] === CLUSTER_IPC_ADVANCED;
|
|
16
19
|
}
|
|
17
20
|
};
|
|
18
21
|
/**
|
|
@@ -51,14 +54,24 @@ var ClusterBackplane = class ClusterBackplane {
|
|
|
51
54
|
#transport;
|
|
52
55
|
#handler = null;
|
|
53
56
|
#started = false;
|
|
57
|
+
/**
|
|
58
|
+
* Récepteur posé sur le transport — une fois pour la vie de l'instance : le
|
|
59
|
+
* transport de production (`process.on("message")`) ne sait pas le retirer, et
|
|
60
|
+
* le reposer à chaque redémarrage doublerait chaque message reçu.
|
|
61
|
+
*/
|
|
62
|
+
#listening = false;
|
|
63
|
+
/** Vrai une fois {@link enableBytes} constaté que le canal porte les octets. */
|
|
64
|
+
#bytes = false;
|
|
54
65
|
constructor(transport = processIpcTransport, originId = resolveBackplaneOriginId()) {
|
|
55
66
|
this.#transport = transport;
|
|
56
67
|
this.originId = originId;
|
|
57
68
|
}
|
|
58
|
-
/** Branche la réception IPC. Idempotent. */
|
|
69
|
+
/** Branche la réception IPC. Idempotent, et redémarrable après {@link stop}. */
|
|
59
70
|
start() {
|
|
60
71
|
if (this.#started) return;
|
|
61
72
|
this.#started = true;
|
|
73
|
+
if (this.#listening) return;
|
|
74
|
+
this.#listening = true;
|
|
62
75
|
this.#transport.onReceive((msg) => this.#ingress(msg));
|
|
63
76
|
}
|
|
64
77
|
/**
|
|
@@ -77,8 +90,19 @@ var ClusterBackplane = class ClusterBackplane {
|
|
|
77
90
|
onMessage(handler) {
|
|
78
91
|
this.#handler = handler;
|
|
79
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Les octets traversent l'IPC tels quels quand le canal est en clonage
|
|
95
|
+
* structuré : aucun codec n'est appliqué, l'argument est ignoré.
|
|
96
|
+
*
|
|
97
|
+
* @returns `true` si le transport constate un canal `advanced`.
|
|
98
|
+
*/
|
|
99
|
+
enableBytes() {
|
|
100
|
+
this.#bytes = this.#transport.carriesBytes?.() === true;
|
|
101
|
+
return this.#bytes;
|
|
102
|
+
}
|
|
80
103
|
/** Tri + anti-echo + délégation au handler. Robuste : ignore tout message non-realtime. */
|
|
81
104
|
#ingress(msg) {
|
|
105
|
+
if (!this.#started) return;
|
|
82
106
|
if (!isEnvelope(msg)) return;
|
|
83
107
|
if (msg.originId === this.originId) return;
|
|
84
108
|
this.#handler?.({
|
|
@@ -87,7 +111,10 @@ var ClusterBackplane = class ClusterBackplane {
|
|
|
87
111
|
originId: msg.originId
|
|
88
112
|
});
|
|
89
113
|
}
|
|
90
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Coupe la réception et détache le handler. Le récepteur du transport survit
|
|
116
|
+
* (`process` ne le retire pas) mais ne livre plus rien jusqu'au prochain {@link start}.
|
|
117
|
+
*/
|
|
91
118
|
stop() {
|
|
92
119
|
this.#handler = null;
|
|
93
120
|
this.#started = false;
|
|
@@ -97,7 +124,8 @@ var ClusterBackplane = class ClusterBackplane {
|
|
|
97
124
|
driver: ClusterBackplane.driver,
|
|
98
125
|
kind: "ipc",
|
|
99
126
|
originId: this.originId,
|
|
100
|
-
crossPod: false
|
|
127
|
+
crossPod: false,
|
|
128
|
+
...this.#bytes && { bytes: true }
|
|
101
129
|
};
|
|
102
130
|
}
|
|
103
131
|
};
|