@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.
- package/README.md +5 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +15 -3
- package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
- package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
- package/dist/nodefony/src/backplane/controlOrder.js +34 -5
- package/dist/nodefony/src/backplane/envelope.js +422 -45
- 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/server/RealtimeConnection.js +31 -6
- package/dist/nodefony/src/server/RealtimeController.js +87 -44
- package/dist/nodefony/src/server/RealtimeHub.js +631 -127
- package/dist/nodefony/src/server/admissionBudget.js +8 -8
- package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/contractRoutes.js +3 -13
- package/dist/nodefony/src/server/frameAdmission.js +27 -35
- 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 +3 -0
- package/dist/nodefony/src/service/RealtimeService.js +9 -0
- package/dist/nodefony/testing/backplaneConformance.js +79 -3
- package/dist/nodefony/testing/index.js +22 -8
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
- 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/RealtimeConnection.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
- 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/contractRoutes.d.ts +1 -1
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
- 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 +3 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
- package/dist/types/nodefony/testing/index.d.ts +1 -1
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +1 -0
- package/docs/architecture.md +21 -6
- package/docs/canaux-a-membres.md +13 -3
- package/docs/cas-d-usage.md +4 -3
- package/docs/configuration.md +38 -2
- package/docs/cookbook-chat.md +400 -152
- package/docs/observabilite.md +28 -9
- package/docs/protocole.md +10 -10
- package/docs/securite.md +64 -10
- package/package.json +12 -12
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import "../src/server/RealtimeController.js";
|
|
1
2
|
import { isRecord } from "../src/guards.js";
|
|
2
3
|
import { createAdmissionResolver, registerControllerPolicies } from "../src/server/channelPolicyRegistry.js";
|
|
3
4
|
import { getRealtimeHub } from "../src/server/RealtimeHub.js";
|
|
4
5
|
import { BACKPLANE_CONFORMANCE_CASES, runBackplaneConformance } from "./backplaneConformance.js";
|
|
5
|
-
import { RequestContext } from "nodefony";
|
|
6
|
+
import { IDENTITY_DEAD_CLOSE_CODE, ORIGIN_REFUSED_CLOSE_CODE, RequestContext } from "nodefony";
|
|
6
7
|
// nodefony/testing/index.ts
|
|
7
8
|
/**
|
|
8
9
|
* `@nodefony/realtime/testing` — **harnais de test d'un controller temps réel**.
|
|
@@ -214,15 +215,28 @@ function createRealtimeHarness(create, options = {}) {
|
|
|
214
215
|
closes,
|
|
215
216
|
notices,
|
|
216
217
|
async connect() {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
const failures = [];
|
|
219
|
+
const syslog = controller.syslog;
|
|
220
|
+
const onLog = (pdu) => {
|
|
221
|
+
const payload = pdu.payload;
|
|
222
|
+
if (pdu.severityName === "ERROR" && typeof payload === "string" && payload.startsWith("WS realtime handshake failed")) failures.push(payload);
|
|
223
|
+
};
|
|
224
|
+
syslog?.on("onLog", onLog);
|
|
225
|
+
try {
|
|
226
|
+
await feed(null);
|
|
227
|
+
hub.onPlatformNotice((message) => {
|
|
228
|
+
notices.push(message);
|
|
229
|
+
});
|
|
230
|
+
await waitFor(() => received.some((f) => f.method === "realtime:welcome") || failures.length > 0, 10);
|
|
231
|
+
} finally {
|
|
232
|
+
syslog?.removeListener("onLog", onLog);
|
|
233
|
+
}
|
|
222
234
|
const welcome = received.find((f) => f.method === "realtime:welcome");
|
|
223
235
|
if (!welcome) {
|
|
224
|
-
const
|
|
225
|
-
|
|
236
|
+
const code = closes[0]?.code;
|
|
237
|
+
const cause = closes.length > 0 ? `socket fermée (code ${String(code)})` : "aucune frame émise";
|
|
238
|
+
const hint = failures.length > 0 ? `le serveur a levé pendant la poignée de main : ${failures[0] ?? ""}` : code === ORIGIN_REFUSED_CLOSE_CODE ? "Origin refusée (option `origin` du harnais)" : code === IDENTITY_DEAD_CLOSE_CODE ? "identité refusée (option `identity` du harnais)" : "vérifier ce que le controller construit à la connexion (services, admissions, base)";
|
|
239
|
+
throw new Error(`createRealtimeHarness: pas de realtime:welcome — ${cause}. ${hint}.`);
|
|
226
240
|
}
|
|
227
241
|
return welcome;
|
|
228
242
|
},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { RealtimeConnection } from "../src/server/RealtimeConnection.js";
|
|
2
|
+
import { PresenceView, REALTIME_PRESENCE_METHOD, isPresenceFrame, ownField } from "nodefony";
|
|
3
|
+
// nodefony/testing/presenceClient.ts
|
|
4
|
+
/**
|
|
5
|
+
* Abonne à `channel` une connexion de `hub` — nommée `user`, ou anonyme (`null`) — et relève les trames de présence qu'elle reçoit, lues et appliquées par le lecteur et la vue du cœur, comme le ferait un navigateur. Une trame que le lecteur refuse est un défaut du serveur : le banc lève en la citant.
|
|
6
|
+
*/
|
|
7
|
+
function presenceClient(hub, channel, user) {
|
|
8
|
+
const frames = [];
|
|
9
|
+
const outcomes = [];
|
|
10
|
+
const view = new PresenceView(channel);
|
|
11
|
+
const codec = hub.textFrameCodec;
|
|
12
|
+
const conn = new RealtimeConnection({
|
|
13
|
+
impose: () => {},
|
|
14
|
+
end: () => false,
|
|
15
|
+
send: (frame) => {
|
|
16
|
+
const data = frame.encodeFor(codec);
|
|
17
|
+
if (typeof data !== "string") return;
|
|
18
|
+
const msg = JSON.parse(data);
|
|
19
|
+
if (ownField(msg, "method") !== REALTIME_PRESENCE_METHOD) return;
|
|
20
|
+
const params = ownField(msg, "params");
|
|
21
|
+
if (!isPresenceFrame(params)) throw new Error(`trame de présence mal formée : ${data}`);
|
|
22
|
+
frames.push(params);
|
|
23
|
+
outcomes.push(view.apply(params));
|
|
24
|
+
}
|
|
25
|
+
}, () => ({
|
|
26
|
+
type: user === null ? "anonymous" : "test",
|
|
27
|
+
authenticated: user !== null,
|
|
28
|
+
id: user,
|
|
29
|
+
userIdentifier: user ?? "anon.",
|
|
30
|
+
roles: [],
|
|
31
|
+
scopes: [],
|
|
32
|
+
issuer: null,
|
|
33
|
+
account: user !== null
|
|
34
|
+
}));
|
|
35
|
+
const sink = () => {};
|
|
36
|
+
if (!hub.subscribeClient(channel, sink, () => () => {}, void 0, conn)) throw new Error(`abonnement à « ${channel} » refusé`);
|
|
37
|
+
hub.presenceSubscribed(channel, conn);
|
|
38
|
+
return {
|
|
39
|
+
frames,
|
|
40
|
+
outcomes,
|
|
41
|
+
view,
|
|
42
|
+
leave: () => {
|
|
43
|
+
hub.unsubscribe(channel, sink);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Présents nommés d'un canal vus par `hub` (identité → onglets), triés. */
|
|
48
|
+
function presenceView(hub, channel) {
|
|
49
|
+
const v = hub.presence(channel);
|
|
50
|
+
const out = {};
|
|
51
|
+
if (v !== null) for (const [k, e] of [...v.members].sort(([a], [b]) => a.localeCompare(b))) out[k] = e.tabs;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export { presenceClient, presenceView };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -112,11 +112,16 @@ export { createSyslogUplinkHandler, BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, } from
|
|
|
112
112
|
export type { SyslogUplinkHandlerOptions, ClientSeverity, } from "./nodefony/src/server/syslogUplink.js";
|
|
113
113
|
export type { ChannelSink, ChannelFactory, ChannelFrameBuilder, } from "./nodefony/src/server/RealtimeHub.js";
|
|
114
114
|
export { ChannelFrame } from "./nodefony/src/server/RealtimeHub.js";
|
|
115
|
+
export { PRESENCE_WINDOW_MS, DEFAULT_PRESENCE_MAX_NAMED, } from "./nodefony/src/server/channelPresence.js";
|
|
116
|
+
export type { IChannelPresenceView } from "./nodefony/src/server/channelPresence.js";
|
|
115
117
|
export type { IBackplane, IBackplaneMessage, BackplaneHandler, IBackplaneInfo, } from "./nodefony/interfaces/IBackplane.js";
|
|
116
118
|
export type { IClusterBackplaneTransport, ClusterBackplaneEnvelope, } from "./nodefony/src/backplane/ClusterBackplane.js";
|
|
117
119
|
export type { IRedisBackplaneTransport, IRedisPublisher, IRedisSubscriber, IRedisBytesSubscriber, } from "./nodefony/src/backplane/RedisBackplane.js";
|
|
118
120
|
export type { BackplaneFactory, IBackplaneFactoryContext, } from "./nodefony/src/backplane/backplaneRegistry.js";
|
|
119
|
-
export { sealBackplaneEnvelope, openBackplaneEnvelope, } from "./nodefony/src/backplane/envelope.js";
|
|
121
|
+
export { sealBackplaneEnvelope, openBackplaneEnvelope, BackplaneEcho, } from "./nodefony/src/backplane/envelope.js";
|
|
122
|
+
export { BACKPLANE_STOP_TIMEOUT_MS, shutdownRealtime, } from "./nodefony/src/server/realtimeShutdown.js";
|
|
123
|
+
export { BackplanePublishQueue, DEFAULT_MAX_QUEUE_BYTES, type BackplaneNotice, type BackplaneNoticeSeverity, } from "./nodefony/src/backplane/publishQueue.js";
|
|
124
|
+
export { BackplaneIngress } from "./nodefony/src/backplane/ingress.js";
|
|
120
125
|
export type { IClusterProbeTransport } from "./nodefony/src/cluster/ClusterProbeClient.js";
|
|
121
126
|
export type { RawWsConnection } from "./nodefony/src/transport/WsConnectionTransport.js";
|
|
122
127
|
export type { IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, IRealtimeChannelStat, IRealtimeConnProbe, } from "./nodefony/interfaces/IRealtimeProbe.js";
|
|
@@ -41,6 +41,18 @@ export declare function isAdmissionClass(allow: ChannelAllow): allow is ChannelA
|
|
|
41
41
|
* @returns la raison du refus, ou `null`.
|
|
42
42
|
*/
|
|
43
43
|
export declare function channelPolicyDefect(value: unknown): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* 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.
|
|
46
|
+
*
|
|
47
|
+
* Sert à refuser deux déclarations CONTRADICTOIRES d'un même nom (une seule
|
|
48
|
+
* s'appliquerait) sans refuser deux déclarations ÉQUIVALENTES, et à ne juger
|
|
49
|
+
* l'écriture que si elle diffère de la lecture. Démarrage seulement.
|
|
50
|
+
*
|
|
51
|
+
* @param a - une politique recevable.
|
|
52
|
+
* @param b - une politique recevable.
|
|
53
|
+
* @returns `true` si aucun pair ne peut satisfaire l'une sans l'autre.
|
|
54
|
+
*/
|
|
55
|
+
export declare function samePolicy(a: IChannelPolicy, b: IChannelPolicy): boolean;
|
|
44
56
|
/**
|
|
45
57
|
* Politique appliquée à une action RPC dont l'auteur n'a rien déclaré :
|
|
46
58
|
* **exiger une connexion authentifiée**. Une action est une méthode que le pair
|
|
@@ -81,9 +93,12 @@ export type RealtimeChannelFactory = (channel: string, publish: RealtimePublish)
|
|
|
81
93
|
* défaut, toute action applicative serait publique sans que rien ne le dise.
|
|
82
94
|
* Pour une action délibérément ouverte, l'écrire : `{ authenticated: false }`.
|
|
83
95
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
96
|
+
* Une politique d'action est une politique d'**écriture** : l'appel exige aussi
|
|
97
|
+
* la lecture déclarée sous le même nom. Sans politique, l'action **hérite** de la
|
|
98
|
+
* lecture d'un canal homonyme (`@RealtimeChannel`) si elle existe — le défaut ne
|
|
99
|
+
* rétrograde jamais une politique déclarée —, et sinon du défaut fermé, quel que
|
|
100
|
+
* soit l'ordre des méthodes. Deux politiques explicites différentes sur le même
|
|
101
|
+
* nom (action et `@RealtimeInbound`) refusent le démarrage.
|
|
87
102
|
*
|
|
88
103
|
* ⚠️ Le NOM peut primer sur ta politique : le plancher système de
|
|
89
104
|
* `@nodefony/security` réserve les préfixes plateforme (`kernel:`, `syslog:`…)
|
|
@@ -136,6 +151,10 @@ type PolicyNamesCheck<P, T> = [NamesNotInContract<P, T>] extends [never] ? unkno
|
|
|
136
151
|
*
|
|
137
152
|
* Un nom qui porte `*` est un motif (famille de canaux), comme partout ailleurs.
|
|
138
153
|
*
|
|
154
|
+
* La politique vaut pour la LECTURE et l'ÉCRITURE du nom (le même objet, jugé
|
|
155
|
+
* une fois). Elle prime sur le défaut fermé d'une action ; contredire une
|
|
156
|
+
* politique explicite d'un décorateur de méthode refuse le démarrage.
|
|
157
|
+
*
|
|
139
158
|
* **Sur un contrôleur de contrat** (`extends ContractRealtimeController<C>`), le
|
|
140
159
|
* décorateur lit le contrat sur la classe qu'il décore : chaque nom doit être
|
|
141
160
|
* une méthode ou un canal de `C`, sinon la classe ne compile pas, et l'erreur
|
|
@@ -153,16 +172,18 @@ export declare function RealtimePolicy<P extends {
|
|
|
153
172
|
readonly [name: string]: IChannelPolicy | undefined;
|
|
154
173
|
}>(policies: P): <T extends abstract new (...args: never[]) => object>(target: T & PolicyNamesCheck<P, T>) => void;
|
|
155
174
|
/**
|
|
156
|
-
* Décorateur **méthode** — expose un **canal pub/sub** sous le nom `channel
|
|
157
|
-
*
|
|
158
|
-
* un `dispose: () => void` (la base le mémorise et l'appelle au dernier
|
|
175
|
+
* Décorateur **méthode** — expose un **canal pub/sub** sous le nom `channel`, ou
|
|
176
|
+
* une FAMILLE de canaux sous un motif (`chat:room:*`). La méthode décorée DOIT
|
|
177
|
+
* renvoyer un `dispose: () => void` (la base le mémorise et l'appelle au dernier
|
|
159
178
|
* désabonné, comme avec l'override classique).
|
|
160
179
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
180
|
+
* Un motif (`*` = toute suite de caractères, la grammaire des politiques) sert
|
|
181
|
+
* chaque nom qu'il couvre : `@RealtimeChannel("chat:room:*", { allow })` déclare
|
|
182
|
+
* la règle ET la fabrique de tous les salons, et la méthode reçoit le nom réel
|
|
183
|
+
* (`chat:room:42`) en premier argument. Ordre de résolution au `subscribe` : le
|
|
184
|
+
* nom exact décoré, puis l'override `createRealtimeChannel()` (canaux à suffixe
|
|
185
|
+
* de granularité, regex — un contrôleur qui en a un garde son comportement),
|
|
186
|
+
* puis les motifs décorés, du plus spécifique au plus général.
|
|
166
187
|
*
|
|
167
188
|
* Le `this` est lié à l'instance ; le factory peut donc lire `this.kernel`, etc.
|
|
168
189
|
* Mais ATTENTION : un provider est **partagé** entre toutes les connexions au
|
|
@@ -170,8 +191,20 @@ export declare function RealtimePolicy<P extends {
|
|
|
170
191
|
* créé. Capturer des **valeurs long-lived** (broker, syslog) dans la closure,
|
|
171
192
|
* jamais `this.context` (lié à la connexion créatrice qui peut fermer).
|
|
172
193
|
*
|
|
194
|
+
* La `policy` est celle de **LECTURE** : exigée au `subscribe`, et en plus de
|
|
195
|
+
* l'écriture sur toute trame entrante du même nom. Absente, la lecture hérite de
|
|
196
|
+
* l'écriture (`@RealtimeInbound`) si elle est déclarée — une application qui ne
|
|
197
|
+
* garde que l'écriture reste protégée en lecture. Pour un canal que tout le
|
|
198
|
+
* monde lit et que seuls certains écrivent, l'écrire : `{ authenticated: false }`.
|
|
199
|
+
*
|
|
173
200
|
* @example
|
|
174
201
|
* class ChatController extends RealtimeController {
|
|
202
|
+
* // Tous les salons : la règle et la fabrique, une seule fois.
|
|
203
|
+
* @RealtimeChannel("chat:room:*", { allow: RoomAdmission })
|
|
204
|
+
* room(_channel: string): () => void {
|
|
205
|
+
* return () => {};
|
|
206
|
+
* }
|
|
207
|
+
*
|
|
175
208
|
* @RealtimeChannel("chat:room42")
|
|
176
209
|
* room42(_channel: string, publish: RealtimePublish): () => void {
|
|
177
210
|
* const timer = setInterval(() => publish("chat:room42", { ts: Date.now() }), 1000);
|
|
@@ -192,6 +225,20 @@ export declare function RealtimeChannel(channel: string, policy?: IChannelPolicy
|
|
|
192
225
|
* pousser tant qu'aucun handler n'est enregistré ici). Cf seam P13.8a
|
|
193
226
|
* (`beforeDispatch`) pour la gate sécurité par voter P6.
|
|
194
227
|
*
|
|
228
|
+
* La `policy` est celle d'**ÉCRITURE** : une trame entrante exige la LECTURE du
|
|
229
|
+
* même nom (`@RealtimeChannel`) ET elle — l'écriture n'est jamais moins stricte
|
|
230
|
+
* que la lecture. Sans lecture déclarée, la lecture en hérite.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* class ChatController extends RealtimeController {
|
|
234
|
+
* @RealtimeChannel("chat:typing", { authenticated: false }) // tout le monde lit
|
|
235
|
+
* typing(_channel: string, _publish: RealtimePublish): () => void {
|
|
236
|
+
* return () => {};
|
|
237
|
+
* }
|
|
238
|
+
* @RealtimeInbound("chat:typing", { authenticated: true }) // seuls les connectés écrivent
|
|
239
|
+
* onTyping(): void {}
|
|
240
|
+
* }
|
|
241
|
+
*
|
|
195
242
|
* @example
|
|
196
243
|
* class ChatController extends RealtimeController {
|
|
197
244
|
* @RealtimeInbound("chat:send")
|
|
@@ -224,6 +271,21 @@ export type RealtimeActionWrapper = (method: RealtimeActionMethod, propertyKey:
|
|
|
224
271
|
export declare function getRealtimeActions(instance: object, wrap?: RealtimeActionWrapper): Record<string, RpcActionHandler> | null;
|
|
225
272
|
/** Lecture (lazy, au handshake) du registre des canaux décorés. */
|
|
226
273
|
export declare function getRealtimeChannels(instance: object): Record<string, RealtimeChannelFactory> | null;
|
|
274
|
+
/**
|
|
275
|
+
* 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.
|
|
276
|
+
*
|
|
277
|
+
* Même grammaire que les politiques (`compileChannelPattern`) et même
|
|
278
|
+
* départage (`channelPatternWeight`, le plus spécifique d'abord) : un nom
|
|
279
|
+
* reçoit la fabrique du motif dont il reçoit la règle. Un motif refusé par la
|
|
280
|
+
* grammaire (trop d'étoiles) ne sert rien — le hub le dit déjà au démarrage.
|
|
281
|
+
* Cold path : appelé au premier abonné d'un canal qu'aucun nom exact ni
|
|
282
|
+
* `createRealtimeChannel` ne produit.
|
|
283
|
+
*
|
|
284
|
+
* @param instance - le contrôleur ; ses motifs se lisent sur sa classe.
|
|
285
|
+
* @param channels - le registre rendu par {@link getRealtimeChannels}.
|
|
286
|
+
* @param channel - le nom demandé.
|
|
287
|
+
*/
|
|
288
|
+
export declare function patternChannelFactory(instance: object, channels: Readonly<Record<string, RealtimeChannelFactory>> | null, channel: string): RealtimeChannelFactory | null;
|
|
227
289
|
/** Lecture (lazy, au handshake) du registre des canaux inbound décorés. */
|
|
228
290
|
export declare function getRealtimeInbound(instance: object): Record<string, RealtimeInboundHandler> | null;
|
|
229
291
|
/**
|
|
@@ -233,14 +295,53 @@ export declare function getRealtimeInbound(instance: object): Record<string, Rea
|
|
|
233
295
|
* @param name - nom d'action ou de canal.
|
|
234
296
|
*/
|
|
235
297
|
export declare function declaresAction(ctor: object, name: string): boolean;
|
|
298
|
+
/** Ce qu'une classe DÉCLARE sous un nom, par sorte — `null` : elle ne pose pas cette sorte. */
|
|
299
|
+
export interface IDeclaredPolicyKinds {
|
|
300
|
+
readonly read: IChannelPolicy | null;
|
|
301
|
+
readonly write: IChannelPolicy | null;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Politiques déclarées par une classe de contrôleur (ADR-0022 §6, décision 1 quater) : ce qu'elle pose par sorte — que le hub résout ensuite avec les motifs des autres classes — et sa vue seule.
|
|
305
|
+
*/
|
|
306
|
+
export interface IDeclaredChannelPolicies {
|
|
307
|
+
/** Vue de la classe SEULE, nom → politique de LECTURE (déclarée, sinon son écriture, sinon le défaut fermé d'une action). */
|
|
308
|
+
readonly read: Record<string, IChannelPolicy>;
|
|
309
|
+
/** Nom → ce que la classe déclare, sorte par sorte (sans héritage : le hub le calcule, motifs compris). */
|
|
310
|
+
readonly kinds: Record<string, IDeclaredPolicyKinds>;
|
|
311
|
+
/** Noms des actions décorées : chacune reçoit le défaut fermé comme REPLI au hub. */
|
|
312
|
+
readonly actions: readonly string[];
|
|
313
|
+
}
|
|
236
314
|
/**
|
|
237
|
-
* Politiques
|
|
315
|
+
* 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.
|
|
316
|
+
*
|
|
317
|
+
* Résolution indépendante de l'ordre des méthodes :
|
|
318
|
+
* - l'écriture d'un nom est celle déclarée (`@RealtimeInbound`, `@RealtimeAction`
|
|
319
|
+
* avec politique, `@RealtimePolicy`) ; pour une action SANS politique, le
|
|
320
|
+
* défaut fermé, sauf si une lecture est déclarée — l'action en hérite alors ;
|
|
321
|
+
* - la lecture est celle déclarée (`@RealtimeChannel`, `@RealtimePolicy`),
|
|
322
|
+
* sinon l'écriture : une application qui ne garde que l'écriture reste
|
|
323
|
+
* protégée en lecture ;
|
|
324
|
+
* - `kinds` garde ce que la classe déclare, SANS héritage : le hub le fusionne
|
|
325
|
+
* avec les motifs de toutes les classes, sorte par sorte.
|
|
326
|
+
*
|
|
327
|
+
* @param ctor - constructeur du contrôleur.
|
|
328
|
+
* @returns les deux tables, ou `null` si la classe ne déclare aucune politique.
|
|
329
|
+
* @throws BootConfigurationError Une politique est illisible, ou deux
|
|
330
|
+
* déclarations d'une même sorte se contredisent : la classe, le canal et le
|
|
331
|
+
* défaut sont nommés.
|
|
332
|
+
*/
|
|
333
|
+
export declare function getDeclaredChannelPolicies(ctor: object): IDeclaredChannelPolicies | null;
|
|
334
|
+
/**
|
|
335
|
+
* 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.
|
|
336
|
+
*
|
|
337
|
+
* La lecture d'un nom est celle exigée au `subscribe` ; une écriture distincte
|
|
338
|
+
* se lit par {@link getDeclaredChannelPolicies}.
|
|
238
339
|
*
|
|
239
340
|
* @param ctor - constructeur du contrôleur.
|
|
240
|
-
* @returns nom de canal ou de méthode → politique, ou `null` si la
|
|
241
|
-
* déclare aucune
|
|
242
|
-
* @throws BootConfigurationError Une politique est illisible
|
|
243
|
-
* canal et le défaut sont nommés.
|
|
341
|
+
* @returns nom de canal ou de méthode → politique de lecture, ou `null` si la
|
|
342
|
+
* classe n'en déclare aucune.
|
|
343
|
+
* @throws BootConfigurationError Une politique est illisible ou contredite :
|
|
344
|
+
* la classe, le canal et le défaut sont nommés.
|
|
244
345
|
*/
|
|
245
346
|
export declare function getChannelPoliciesOf(ctor: object): Record<string, IChannelPolicy> | null;
|
|
246
347
|
/**
|
|
@@ -62,6 +62,10 @@ export type BackplaneHandler = (msg: IBackplaneMessage) => void;
|
|
|
62
62
|
* - **aucun rejeu au démarrage** : lire à partir du dernier message, jamais depuis le
|
|
63
63
|
* début — un ordre de fin rejoué couperait une partie rouverte ;
|
|
64
64
|
* - **aucune attente de regroupement** à l'émission : `publish` part tout de suite ;
|
|
65
|
+
* - **un ordre de contrôle n'est jamais jeté pour faire place aux publications**
|
|
66
|
+
* (`PLATFORM_BUS_CHANNELS.control` : terminer un canal, éjecter un compte) —
|
|
67
|
+
* une file d'envoi bornée lui laisse une voie hors quota ; un ordre perdu malgré
|
|
68
|
+
* tout (bus injoignable) se compte et s'annonce, jamais en silence ;
|
|
65
69
|
* - **le sceau appliqué** dès que le bus quitte la machine (`crossPod`) :
|
|
66
70
|
* `sealBackplaneEnvelope` à l'émission, `openBackplaneEnvelope` à la réception ;
|
|
67
71
|
* - `start` après `stop` remet la réception en route sans doubler les messages ;
|
|
@@ -101,7 +105,14 @@ export interface IBackplane {
|
|
|
101
105
|
* @returns `true` si les octets traverseront le bus intacts.
|
|
102
106
|
*/
|
|
103
107
|
enableBytes?(codec: IFrameCodec): boolean;
|
|
104
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Arrête le transport et libère les ressources (connexions, listeners). Idempotent.
|
|
110
|
+
*
|
|
111
|
+
* Un transport asynchrone laisse d'abord partir ce qui est déjà émis (la promesse
|
|
112
|
+
* se résout une fois ces envois acquittés ou refusés) : à l'arrêt du processus,
|
|
113
|
+
* la preuve de vie finale de la présence part juste avant, et la sortie suit au
|
|
114
|
+
* tick suivant. L'appelant borne l'attente (`shutdownRealtime`).
|
|
115
|
+
*/
|
|
105
116
|
stop(): void | Promise<void>;
|
|
106
117
|
/**
|
|
107
118
|
* Carte d'identité du backplane (driver, transport, origine, cross-pod, canal).
|
|
@@ -222,12 +222,20 @@ export declare class RedisBackplane implements IBackplane {
|
|
|
222
222
|
* L'envoi passe par la **file bornée** : si le bus n'acquitte plus, les
|
|
223
223
|
* publications suivantes sont jetées et comptées plutôt que d'empiler des
|
|
224
224
|
* mégaoctets dans le client Redis (sémantique at-most-once déjà assumée par le
|
|
225
|
-
* port ; le client realtime re-synchronise).
|
|
225
|
+
* port ; le client realtime re-synchronise). Un ordre de contrôle, lui, part
|
|
226
|
+
* toujours (cf {@link BackplanePublishQueue.send}).
|
|
226
227
|
*/
|
|
227
228
|
publish(channel: string, payload: unknown): void;
|
|
228
229
|
/** Un seul handler d'ingress ; un appel ultérieur remplace le précédent. */
|
|
229
230
|
onMessage(handler: BackplaneHandler): void;
|
|
230
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Laisse partir les publications déjà émises (acquittées ou refusées par Redis), puis désabonne du canal et détache le handler. Idempotent.
|
|
233
|
+
*
|
|
234
|
+
* Sans l'attente, un arrêt suivi de la sortie du processus perdait ce qui
|
|
235
|
+
* était encore dans la file — la preuve de vie FINALE de la présence comprise,
|
|
236
|
+
* et les pairs gardaient alors ce pod jusqu'au délai d'oubli. Un Redis qui ne
|
|
237
|
+
* répond plus fait attendre : l'appelant borne (l'arrêt du module le fait).
|
|
238
|
+
*/
|
|
231
239
|
stop(): Promise<void>;
|
|
232
240
|
describe(): IBackplaneInfo;
|
|
233
241
|
}
|
|
@@ -35,6 +35,29 @@ export interface IEvictUserOrder {
|
|
|
35
35
|
}
|
|
36
36
|
/** Un ordre de contrôle reconnu. */
|
|
37
37
|
export type ControlOrder = ICloseChannelOrder | IEvictUserOrder;
|
|
38
|
+
/**
|
|
39
|
+
* Un texte venu du bus est-il recevable — chaîne non vide d'au plus {@link MAX_CONTROL_FIELD_LENGTH} caractères ?
|
|
40
|
+
*
|
|
41
|
+
* Seule borne des textes du bus (nom de canal, identifiant de compte), lue par
|
|
42
|
+
* les ordres de contrôle ET par les messages de présence (ADR-0025 P2).
|
|
43
|
+
*
|
|
44
|
+
* @param value - champ reçu (forme inconnue).
|
|
45
|
+
*/
|
|
46
|
+
export declare function isBoundedBusText(value: unknown): value is string;
|
|
47
|
+
/**
|
|
48
|
+
* La charge est-elle un ordre d'un TYPE que cette version ne connaît pas — `op` lisible, mais hors de la liste fermée ?
|
|
49
|
+
*
|
|
50
|
+
* C'est ce qu'émet un serveur d'une version plus récente pendant un
|
|
51
|
+
* déploiement progressif (ADR-0025, « Règle de lecture du bus ») : l'ordre
|
|
52
|
+
* n'est pas appliqué, mais il ne se compte pas comme une injection. Rien
|
|
53
|
+
* d'autre que `op` n'est lu — ses autres champs appartiennent à une version
|
|
54
|
+
* qui n'est pas celle-ci. Une charge sans `op` textuel, ou d'un type CONNU de
|
|
55
|
+
* forme fausse, n'est pas « inconnue » : elle est invalide.
|
|
56
|
+
*
|
|
57
|
+
* @param payload - la charge du message reçu (forme inconnue).
|
|
58
|
+
* @returns `true` si le type d'ordre est inconnu de cette version.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isUnknownControlOrder(payload: unknown): boolean;
|
|
38
61
|
/**
|
|
39
62
|
* Lit un ordre de contrôle dans une charge venue du bus — `null` si la forme n'est pas celle d'un ordre connu.
|
|
40
63
|
*
|
|
@@ -20,6 +20,10 @@ export declare function sealBackplaneEnvelope(msg: IBackplaneMessage, secret?: s
|
|
|
20
20
|
* `sig`. Sans secret, l'enveloppe nue est acceptée (compat ; l'alerte de boot qui
|
|
21
21
|
* signale ce mode non authentifié est portée par le wiring du driver).
|
|
22
22
|
*
|
|
23
|
+
* Le sceau se vérifie sur la tranche REÇUE de la charge quand l'enveloppe a la
|
|
24
|
+
* forme qu'écrit {@link sealBackplaneEnvelope} — aucune re-sérialisation ; une
|
|
25
|
+
* autre forme retombe sur la re-sérialisation de la charge lue.
|
|
26
|
+
*
|
|
23
27
|
* @param raw - message brut lu sur le bus.
|
|
24
28
|
* @param secret - secret partagé attendu, ou `null`/`undefined` si aucun.
|
|
25
29
|
* @returns le message vérifié, ou `null` s'il doit être ignoré (malformé, non
|
|
@@ -32,9 +36,10 @@ export declare function openBackplaneEnvelope(raw: string, secret?: string | nul
|
|
|
32
36
|
* binaire du serveur, octets natifs dans `payload`, scellée si un secret est
|
|
33
37
|
* fourni.
|
|
34
38
|
*
|
|
35
|
-
* Scellée, la charge est encodée
|
|
36
|
-
* l'enveloppe
|
|
37
|
-
*
|
|
39
|
+
* Scellée, la charge est encodée UNE fois : le sceau se calcule sur ses octets,
|
|
40
|
+
* et l'enveloppe s'assemble autour d'eux. Un codec qui n'écrit pas le CBOR le plus
|
|
41
|
+
* court (constaté, cf {@link framesAsCbor}) encode l'enveloppe entière une seconde
|
|
42
|
+
* fois — correct pour tout format, plus coûteux.
|
|
38
43
|
*
|
|
39
44
|
* @param msg - message à transporter (canal, charge, origine).
|
|
40
45
|
* @param codec - le codec binaire du serveur (`CBOR_FRAME_CODEC` de `nodefony/cbor`).
|
|
@@ -48,10 +53,10 @@ export declare function sealBackplaneEnvelopeBytes(msg: IBackplaneMessage, codec
|
|
|
48
53
|
* règle que {@link openBackplaneEnvelope} : fail-closed strict sous un secret,
|
|
49
54
|
* aucun downgrade en retirant `sig`.
|
|
50
55
|
*
|
|
51
|
-
* Le sceau se
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* octets.
|
|
56
|
+
* Le sceau se vérifie sur la tranche REÇUE de la charge quand l'enveloppe a la
|
|
57
|
+
* forme qu'assemble l'émetteur — aucun ré-encodage ; une autre forme retombe sur
|
|
58
|
+
* le ré-encodage de la charge décodée (le codec CBOR du cœur rend la forme la plus
|
|
59
|
+
* courte de chaque valeur, donc émetteur et récepteur signent les mêmes octets).
|
|
55
60
|
*
|
|
56
61
|
* @param raw - octets lus sur le sujet binaire.
|
|
57
62
|
* @param codec - le codec binaire du serveur.
|
|
@@ -59,3 +64,41 @@ export declare function sealBackplaneEnvelopeBytes(msg: IBackplaneMessage, codec
|
|
|
59
64
|
* @returns le message vérifié, ou `null` s'il doit être ignoré.
|
|
60
65
|
*/
|
|
61
66
|
export declare function openBackplaneEnvelopeBytes(raw: Uint8Array, codec: IFrameCodec, secret?: string | null): IBackplaneMessage | null;
|
|
67
|
+
/**
|
|
68
|
+
* Reconnaît l'**écho** d'un pod — ce que le bus lui renvoie de ses propres
|
|
69
|
+
* publications — à la FIN du message, sans le décoder (ADR-0021 S6, ADR-0025).
|
|
70
|
+
*
|
|
71
|
+
* Un bus pub/sub comme Redis rend à l'émetteur chacune de ses publications : sur un
|
|
72
|
+
* pod chargé, c'est un message reçu sur deux, et la présence répartie en fait un
|
|
73
|
+
* signal (le bus est vivant tant que l'écho revient). Le reconnaître coûte, sous un
|
|
74
|
+
* secret, UN HMAC sur la tranche de la charge : ni `JSON.parse`, ni décodage CBOR.
|
|
75
|
+
*
|
|
76
|
+
* Sous un secret, l'écho est VÉRIFIÉ avant d'être compté : un tiers qui forgerait
|
|
77
|
+
* l'origine de ce pod pour simuler un bus vivant échoue au sceau. Sans secret, rien
|
|
78
|
+
* ne peut l'être — le bus est ouvert, et le wiring l'annonce au démarrage.
|
|
79
|
+
*
|
|
80
|
+
* Un message que ce lecteur ne reconnaît pas (forme tierce, sceau faux) n'est PAS
|
|
81
|
+
* jeté : il suit la voie ordinaire, qui le vérifie en entier.
|
|
82
|
+
*/
|
|
83
|
+
export declare class BackplaneEcho {
|
|
84
|
+
#private;
|
|
85
|
+
/**
|
|
86
|
+
* @param originId - identité de CE pod sur le bus.
|
|
87
|
+
* @param secret - secret partagé, ou `null` sur un bus non scellé.
|
|
88
|
+
*/
|
|
89
|
+
constructor(originId: string, secret: string | null);
|
|
90
|
+
/**
|
|
91
|
+
* Ce message texte est-il l'écho de ce pod (sceau vérifié sous un secret) ?
|
|
92
|
+
*
|
|
93
|
+
* @param raw - message brut lu sur le sujet texte.
|
|
94
|
+
*/
|
|
95
|
+
text(raw: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Ce message binaire est-il l'écho de ce pod (sceau vérifié sous un secret) ?
|
|
98
|
+
*
|
|
99
|
+
* @param raw - octets lus sur le sujet binaire.
|
|
100
|
+
* @param codec - le codec binaire du serveur ; un codec qui n'écrit pas le CBOR
|
|
101
|
+
* attendu (cf {@link framesAsCbor}) laisse tout message à la voie ordinaire.
|
|
102
|
+
*/
|
|
103
|
+
bytes(raw: Uint8Array, codec: IFrameCodec): boolean;
|
|
104
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IFrameCodec } from "nodefony";
|
|
2
|
+
import type { IBackplaneMessage } from "../../interfaces/IBackplane.js";
|
|
3
|
+
import type { BackplaneNotice } from "./publishQueue.js";
|
|
4
|
+
/**
|
|
5
|
+
* **Entrée d'un bus partagé** — le tri de ce qu'un pilote reçoit, mutualisé pour
|
|
6
|
+
* que tout pilote à transport réseau (Redis aujourd'hui ; Kafka, NATS, RabbitMQ…
|
|
7
|
+
* demain) l'hérite sans le réécrire : écho de ce pod reconnu et compté, sceau
|
|
8
|
+
* vérifié, refus comptés et annoncés.
|
|
9
|
+
*
|
|
10
|
+
* Le pilote ne garde que son transport : il passe chaque message reçu à
|
|
11
|
+
* {@link text} ou {@link bytes} et remet au hub ce qui en sort.
|
|
12
|
+
*
|
|
13
|
+
* Ordre du tri, du moins cher au plus cher :
|
|
14
|
+
* 1. l'ÉCHO de ce pod se reconnaît à la fin du message, sans le décoder — un HMAC
|
|
15
|
+
* sous un secret (cf {@link BackplaneEcho}) ;
|
|
16
|
+
* 2. sinon, l'enveloppe est ouverte et son sceau vérifié sur la tranche reçue ;
|
|
17
|
+
* 3. un message ouvert qui porte l'origine de ce pod sous une autre forme est
|
|
18
|
+
* encore un écho — compté, jamais réinjecté (anti-boucle).
|
|
19
|
+
*/
|
|
20
|
+
export declare class BackplaneIngress {
|
|
21
|
+
#private;
|
|
22
|
+
/**
|
|
23
|
+
* @param originId - identité de CE pod sur le bus.
|
|
24
|
+
* @param secret - secret partagé, ou `null` sur un bus non scellé.
|
|
25
|
+
* @param notice - annonce des sceaux refusés ; omis = silencieux (tests).
|
|
26
|
+
*/
|
|
27
|
+
constructor(originId: string, secret: string | null, notice?: BackplaneNotice | null);
|
|
28
|
+
/**
|
|
29
|
+
* Trie un message reçu sur le sujet TEXTE du bus.
|
|
30
|
+
*
|
|
31
|
+
* @param raw - message brut tel que lu sur le transport.
|
|
32
|
+
* @returns le message à remettre au hub, ou `null` s'il est jeté (écho de ce
|
|
33
|
+
* pod, malformé, sceau refusé — chacun compté).
|
|
34
|
+
*/
|
|
35
|
+
text(raw: string): IBackplaneMessage | null;
|
|
36
|
+
/**
|
|
37
|
+
* Trie un message reçu sur le sujet BINAIRE du bus (ADR-0021 S6).
|
|
38
|
+
*
|
|
39
|
+
* @param raw - octets tels que lus sur le transport.
|
|
40
|
+
* @param codec - le codec binaire du serveur.
|
|
41
|
+
* @returns le message à remettre au hub, ou `null` s'il est jeté.
|
|
42
|
+
*/
|
|
43
|
+
bytes(raw: Uint8Array, codec: IFrameCodec): IBackplaneMessage | null;
|
|
44
|
+
/** Échos de ce pod reçus du bus (monotone) — le bus rend ce qu'on y publie. */
|
|
45
|
+
get echoTotal(): number;
|
|
46
|
+
/** Messages jetés au sceau refusé, sous un secret (monotone). */
|
|
47
|
+
get sealRejectedTotal(): number;
|
|
48
|
+
}
|