@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,7 +1,9 @@
|
|
|
1
|
-
import { isCallable, isRecord, isThenable } from "../guards.js";
|
|
2
1
|
import resolveBackplaneOriginId from "./originId.js";
|
|
3
|
-
import {
|
|
2
|
+
import { isCallable, isRecord, isThenable } from "../guards.js";
|
|
3
|
+
import { sealBackplaneEnvelope, sealBackplaneEnvelopeBytes } from "./envelope.js";
|
|
4
|
+
import { BackplaneIngress } from "./ingress.js";
|
|
4
5
|
import { BackplanePublishQueue } from "./publishQueue.js";
|
|
6
|
+
import { containsBytes } from "nodefony";
|
|
5
7
|
// nodefony/src/backplane/RedisBackplane.ts
|
|
6
8
|
/**
|
|
7
9
|
* BASE du canal Redis **dédié** transportant les enveloppes realtime entre pods.
|
|
@@ -28,22 +30,18 @@ function resolveRedisChannel(namespace) {
|
|
|
28
30
|
return namespace ? `${REDIS_RT_CHANNEL}:${namespace}` : REDIS_RT_CHANNEL;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* Construit le **sujet binaire** du bus (ADR-0021 S6) : le canal texte suffixé du
|
|
34
|
+
* protocole du codec. Un second sujet, versionné par son nom, que les exemplaires
|
|
35
|
+
* plus anciens n'écoutent pas — le message texte n'est pas touché, et un codec
|
|
36
|
+
* binaire d'un autre format aurait son propre sujet.
|
|
35
37
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* createRedisServiceTransport(redis.getClient("publish")!, redis.getClient("subscribe")!),
|
|
40
|
-
* );
|
|
41
|
-
* defineRealtimeConfig({ backplane: { driver: "redis" } }, { backplane: bp });
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* Le listener Redis est conservé pour un `unsubscribe(channel, listener)` ciblé
|
|
45
|
-
* (n'arrache pas d'éventuels autres abonnés du même client).
|
|
38
|
+
* @param redisChannel - le canal texte effectif ({@link resolveRedisChannel}).
|
|
39
|
+
* @param codec - le codec binaire du serveur.
|
|
40
|
+
* @returns le nom du canal Redis qui porte les enveloppes binaires.
|
|
46
41
|
*/
|
|
42
|
+
function resolveRedisBytesChannel(redisChannel, codec) {
|
|
43
|
+
return `${redisChannel}:${codec.protocol}`;
|
|
44
|
+
}
|
|
47
45
|
/**
|
|
48
46
|
* Vrai si la valeur expose la surface d'un client Redis publisher — vérifié au
|
|
49
47
|
* démarrage, sur le client que rend le service `redis`.
|
|
@@ -55,27 +53,77 @@ function isRedisPublisher(value) {
|
|
|
55
53
|
function isRedisSubscriber(value) {
|
|
56
54
|
return isRecord(value) && isCallable(value.subscribe) && isCallable(value.unsubscribe);
|
|
57
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Vrai si la valeur expose la surface d'un client abonné capable du mode octets.
|
|
58
|
+
*
|
|
59
|
+
* La forme ne dit pas si le troisième argument est compris : ce garde vaut pour
|
|
60
|
+
* le client que rend le service `redis` (node-redis, qui l'accepte). Un client qui
|
|
61
|
+
* l'ignorerait livrerait du texte au sujet binaire — illisible pour le codec,
|
|
62
|
+
* donc jeté, compté sous un secret (`sealRejectedTotal`), jamais réinjecté.
|
|
63
|
+
*/
|
|
64
|
+
function isRedisBytesSubscriber(value) {
|
|
65
|
+
return isRedisSubscriber(value);
|
|
66
|
+
}
|
|
58
67
|
/** Ramène l'acquittement d'une commande Redis au contrat du transport. */
|
|
59
68
|
function settled(ack) {
|
|
60
69
|
return isThenable(ack) ? ack.then(() => void 0) : void 0;
|
|
61
70
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Adapte deux clients Redis (publisher + subscriber dédié) au seam
|
|
73
|
+
* {@link IRedisBackplaneTransport}. SEUL point de contact avec la lib `redis` —
|
|
74
|
+
* et même ici le couplage est **structurel** (aucun import). Le consommateur
|
|
75
|
+
* branche typiquement les connexions du module `@nodefony/redis` :
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* const redis = kernel.get("redis"); // RedisService
|
|
79
|
+
* const bp = new RedisBackplane(
|
|
80
|
+
* createRedisServiceTransport(redis.getClient("publish")!, redis.getClient("subscribe")!),
|
|
81
|
+
* );
|
|
82
|
+
* defineRealtimeConfig({ backplane: { driver: "redis" } }, { backplane: bp });
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* Chaque listener Redis est conservé, par canal, pour un `unsubscribe(channel,
|
|
86
|
+
* listener)` ciblé (n'arrache pas d'éventuels autres abonnés du même client).
|
|
87
|
+
*
|
|
88
|
+
* @param publisher - connexion de publication.
|
|
89
|
+
* @param subscriber - connexion d'abonnement dédiée.
|
|
90
|
+
* @param bytesSubscriber - la MÊME connexion d'abonnement, vue en mode octets
|
|
91
|
+
* ({@link IRedisBytesSubscriber}) ; absente, le transport n'offre pas de sujet
|
|
92
|
+
* binaire et les octets restent sur le serveur qui les publie.
|
|
93
|
+
* @returns le transport du {@link RedisBackplane}.
|
|
94
|
+
*/
|
|
95
|
+
function createRedisServiceTransport(publisher, subscriber, bytesSubscriber) {
|
|
96
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
97
|
+
const transport = {
|
|
65
98
|
publish(channel, message) {
|
|
66
|
-
const ack = publisher.publish(channel, message);
|
|
99
|
+
const ack = publisher.publish(channel, typeof message === "string" ? message : Buffer.from(message.buffer, message.byteOffset, message.byteLength));
|
|
67
100
|
return isThenable(ack) ? ack : void 0;
|
|
68
101
|
},
|
|
69
102
|
subscribe(channel, onMessage) {
|
|
70
|
-
|
|
71
|
-
|
|
103
|
+
const fn = (message) => onMessage(message);
|
|
104
|
+
listeners.set(channel, {
|
|
105
|
+
bytes: false,
|
|
106
|
+
fn
|
|
107
|
+
});
|
|
108
|
+
return settled(subscriber.subscribe(channel, fn));
|
|
72
109
|
},
|
|
73
110
|
unsubscribe(channel) {
|
|
74
|
-
const current =
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
const current = listeners.get(channel);
|
|
112
|
+
listeners.delete(channel);
|
|
113
|
+
if (current === void 0) return settled(subscriber.unsubscribe(channel));
|
|
114
|
+
if (!current.bytes) return settled(subscriber.unsubscribe(channel, current.fn));
|
|
115
|
+
return settled(bytesSubscriber?.unsubscribe(channel, current.fn, true));
|
|
77
116
|
}
|
|
78
117
|
};
|
|
118
|
+
if (bytesSubscriber !== void 0) transport.subscribeBytes = (channel, onMessage) => {
|
|
119
|
+
const fn = (message) => onMessage(message);
|
|
120
|
+
listeners.set(channel, {
|
|
121
|
+
bytes: true,
|
|
122
|
+
fn
|
|
123
|
+
});
|
|
124
|
+
return settled(bytesSubscriber.subscribe(channel, fn, true));
|
|
125
|
+
};
|
|
126
|
+
return transport;
|
|
79
127
|
}
|
|
80
128
|
/**
|
|
81
129
|
* Backplane **Redis pub/sub** (P13.5) — implémentation du port {@link IBackplane}
|
|
@@ -119,14 +167,24 @@ var RedisBackplane = class RedisBackplane {
|
|
|
119
167
|
#secret;
|
|
120
168
|
/** Borne mémoire des publications en vol (cf {@link BackplanePublishQueue}). */
|
|
121
169
|
#queue;
|
|
170
|
+
#notice;
|
|
171
|
+
/** Tri de l'entrée : écho, sceau, refus comptés (brique commune aux pilotes). */
|
|
172
|
+
#ingress;
|
|
122
173
|
#handler = null;
|
|
123
174
|
#started = false;
|
|
175
|
+
/**
|
|
176
|
+
* Sujet binaire : son codec et son canal Redis ({@link resolveRedisBytesChannel}) ;
|
|
177
|
+
* `null` tant que {@link enableBytes} n'a pas armé les octets.
|
|
178
|
+
*/
|
|
179
|
+
#bytes = null;
|
|
124
180
|
constructor(transport, originId = resolveBackplaneOriginId(), redisChannel = REDIS_RT_CHANNEL, secret = null, options = {}) {
|
|
125
181
|
this.#transport = transport;
|
|
126
182
|
this.originId = originId;
|
|
127
183
|
this.#redisChannel = redisChannel;
|
|
128
184
|
this.#secret = secret;
|
|
129
|
-
this.#
|
|
185
|
+
this.#notice = options.onNotice ?? null;
|
|
186
|
+
this.#queue = new BackplanePublishQueue(options.maxQueueBytes ?? 8388608, this.#notice);
|
|
187
|
+
this.#ingress = new BackplaneIngress(originId, secret, this.#notice);
|
|
130
188
|
}
|
|
131
189
|
/**
|
|
132
190
|
* Abonne au canal Redis dédié. Idempotent (le flag est posé avant l'`await` →
|
|
@@ -135,7 +193,32 @@ var RedisBackplane = class RedisBackplane {
|
|
|
135
193
|
async start() {
|
|
136
194
|
if (this.#started) return;
|
|
137
195
|
this.#started = true;
|
|
138
|
-
await this.#transport.subscribe(this.#redisChannel, (raw) => this.#
|
|
196
|
+
await Promise.all([this.#transport.subscribe(this.#redisChannel, (raw) => this.#receive(raw)), this.#subscribeBytes()]);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Arme le sujet binaire (ADR-0021 S6) : les charges qui contiennent des octets
|
|
200
|
+
* partent en enveloppe encodée par `codec` sur un second canal Redis, et ce
|
|
201
|
+
* canal est écouté. Les charges sans octets gardent le sujet texte, intact pour
|
|
202
|
+
* les exemplaires plus anciens. Idempotent.
|
|
203
|
+
*
|
|
204
|
+
* @param codec - le codec binaire du serveur.
|
|
205
|
+
* @returns `false` si le transport ne sait pas s'abonner en octets.
|
|
206
|
+
*/
|
|
207
|
+
enableBytes(codec) {
|
|
208
|
+
if (this.#transport.subscribeBytes === void 0) return false;
|
|
209
|
+
if (this.#bytes !== null) return true;
|
|
210
|
+
this.#bytes = {
|
|
211
|
+
codec,
|
|
212
|
+
channel: resolveRedisBytesChannel(this.#redisChannel, codec)
|
|
213
|
+
};
|
|
214
|
+
if (this.#started) this.#subscribeBytes().catch((err) => this.#notice?.(`backplane : abonnement au sujet binaire en échec — les octets des autres exemplaires ne seront pas reçus (${err instanceof Error ? err.message : String(err)})`, "WARNING"));
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
/** Abonne le sujet binaire s'il est armé ; sinon rien. */
|
|
218
|
+
async #subscribeBytes() {
|
|
219
|
+
const bytes = this.#bytes;
|
|
220
|
+
if (bytes === null) return;
|
|
221
|
+
await this.#transport.subscribeBytes?.(bytes.channel, (raw) => this.#receiveBytes(raw, bytes.codec));
|
|
139
222
|
}
|
|
140
223
|
/**
|
|
141
224
|
* Propage une publication locale aux autres pods via Redis. NE refait PAS le
|
|
@@ -144,7 +227,8 @@ var RedisBackplane = class RedisBackplane {
|
|
|
144
227
|
* L'envoi passe par la **file bornée** : si le bus n'acquitte plus, les
|
|
145
228
|
* publications suivantes sont jetées et comptées plutôt que d'empiler des
|
|
146
229
|
* mégaoctets dans le client Redis (sémantique at-most-once déjà assumée par le
|
|
147
|
-
* port ; le client realtime re-synchronise).
|
|
230
|
+
* port ; le client realtime re-synchronise). Un ordre de contrôle, lui, part
|
|
231
|
+
* toujours (cf {@link BackplanePublishQueue.send}).
|
|
148
232
|
*/
|
|
149
233
|
publish(channel, payload) {
|
|
150
234
|
const env = {
|
|
@@ -152,30 +236,48 @@ var RedisBackplane = class RedisBackplane {
|
|
|
152
236
|
payload,
|
|
153
237
|
originId: this.originId
|
|
154
238
|
};
|
|
239
|
+
const bytes = this.#bytes;
|
|
240
|
+
if (bytes !== null && containsBytes(payload)) {
|
|
241
|
+
const bin = sealBackplaneEnvelopeBytes(env, bytes.codec, this.#secret);
|
|
242
|
+
this.#queue.send(channel, bin.byteLength, () => this.#transport.publish(bytes.channel, bin));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
155
245
|
const raw = sealBackplaneEnvelope(env, this.#secret);
|
|
156
|
-
this.#queue.send(Buffer.byteLength(raw), () => this.#transport.publish(this.#redisChannel, raw));
|
|
246
|
+
this.#queue.send(channel, Buffer.byteLength(raw), () => this.#transport.publish(this.#redisChannel, raw));
|
|
157
247
|
}
|
|
158
248
|
/** Un seul handler d'ingress ; un appel ultérieur remplace le précédent. */
|
|
159
249
|
onMessage(handler) {
|
|
160
250
|
this.#handler = handler;
|
|
161
251
|
}
|
|
162
252
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
253
|
+
* Remet au hub ce que le tri d'entrée admet ({@link BackplaneIngress}) : l'écho
|
|
254
|
+
* de ce pod (Redis renvoie à l'émetteur) est compté sans être décodé ; un message
|
|
255
|
+
* malformé ou au sceau refusé est **ignoré** — jamais réinjecté, jamais fatal —,
|
|
256
|
+
* compté et annoncé la première fois puis une fois sur cent (ADR-0021 S6).
|
|
166
257
|
*/
|
|
167
|
-
#
|
|
168
|
-
const msg =
|
|
169
|
-
if (msg
|
|
170
|
-
|
|
171
|
-
|
|
258
|
+
#receive(raw) {
|
|
259
|
+
const msg = this.#ingress.text(raw);
|
|
260
|
+
if (msg !== null) this.#handler?.(msg);
|
|
261
|
+
}
|
|
262
|
+
/** Même tri que {@link #receive}, sur le sujet binaire. */
|
|
263
|
+
#receiveBytes(raw, codec) {
|
|
264
|
+
const msg = this.#ingress.bytes(raw, codec);
|
|
265
|
+
if (msg !== null) this.#handler?.(msg);
|
|
172
266
|
}
|
|
173
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* 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.
|
|
269
|
+
*
|
|
270
|
+
* Sans l'attente, un arrêt suivi de la sortie du processus perdait ce qui
|
|
271
|
+
* était encore dans la file — la preuve de vie FINALE de la présence comprise,
|
|
272
|
+
* et les pairs gardaient alors ce pod jusqu'au délai d'oubli. Un Redis qui ne
|
|
273
|
+
* répond plus fait attendre : l'appelant borne (l'arrêt du module le fait).
|
|
274
|
+
*/
|
|
174
275
|
async stop() {
|
|
175
276
|
this.#handler = null;
|
|
176
277
|
if (!this.#started) return;
|
|
177
278
|
this.#started = false;
|
|
178
|
-
await this.#
|
|
279
|
+
await this.#queue.settled();
|
|
280
|
+
await Promise.all([this.#transport.unsubscribe(this.#redisChannel), this.#bytes !== null ? this.#transport.unsubscribe(this.#bytes.channel) : void 0]);
|
|
179
281
|
}
|
|
180
282
|
describe() {
|
|
181
283
|
return {
|
|
@@ -185,8 +287,11 @@ var RedisBackplane = class RedisBackplane {
|
|
|
185
287
|
crossPod: true,
|
|
186
288
|
channel: this.#redisChannel,
|
|
187
289
|
sealed: this.#secret !== null,
|
|
188
|
-
|
|
290
|
+
...this.#secret !== null && { sealRejectedTotal: this.#ingress.sealRejectedTotal },
|
|
291
|
+
echoTotal: this.#ingress.echoTotal,
|
|
292
|
+
queue: this.#queue.describe(),
|
|
293
|
+
...this.#bytes !== null && { bytes: true }
|
|
189
294
|
};
|
|
190
295
|
}
|
|
191
296
|
};
|
|
192
|
-
export { REDIS_RT_CHANNEL, RedisBackplane, RedisBackplane as default, createRedisServiceTransport, isRedisPublisher, isRedisSubscriber, resolveRedisChannel };
|
|
297
|
+
export { REDIS_RT_CHANNEL, RedisBackplane, RedisBackplane as default, createRedisServiceTransport, isRedisBytesSubscriber, isRedisPublisher, isRedisSubscriber, resolveRedisBytesChannel, resolveRedisChannel };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ownField } from "nodefony";
|
|
2
|
+
// nodefony/src/backplane/controlOrder.ts
|
|
3
|
+
/**
|
|
4
|
+
* Ordres de contrôle entre serveurs — la LISTE FERMÉE de ce qui circule sur le
|
|
5
|
+
* canal `PLATFORM_BUS_CHANNELS.control` (ADR-0022 §6 décision 4).
|
|
6
|
+
*
|
|
7
|
+
* Un ordre ne fait qu'ENLEVER : terminer un canal, éjecter un compte d'un
|
|
8
|
+
* canal. Aucun n'admet — seul `allow` fait entrer. Il voyage dans la charge
|
|
9
|
+
* d'un message ordinaire du bus, donc sous le sceau de l'enveloppe
|
|
10
|
+
* (`envelope.ts#sealBackplaneEnvelope`), sans champ neuf dans l'enveloppe ni
|
|
11
|
+
* méthode neuve dans `IBackplane` : un driver tiers le transporte sans le savoir.
|
|
12
|
+
*/
|
|
13
|
+
/** Longueur maximale d'un champ texte d'un ordre (nom de canal, identifiant de compte). */
|
|
14
|
+
const MAX_CONTROL_FIELD_LENGTH = 1024;
|
|
15
|
+
/**
|
|
16
|
+
* Un texte venu du bus est-il recevable — chaîne non vide d'au plus {@link MAX_CONTROL_FIELD_LENGTH} caractères ?
|
|
17
|
+
*
|
|
18
|
+
* Seule borne des textes du bus (nom de canal, identifiant de compte), lue par
|
|
19
|
+
* les ordres de contrôle ET par les messages de présence (ADR-0025 P2).
|
|
20
|
+
*
|
|
21
|
+
* @param value - champ reçu (forme inconnue).
|
|
22
|
+
*/
|
|
23
|
+
function isBoundedBusText(value) {
|
|
24
|
+
return typeof value === "string" && value.length > 0 && value.length <= 1024;
|
|
25
|
+
}
|
|
26
|
+
/** Les types d'ordre que cette version sait appliquer — liste fermée. */
|
|
27
|
+
function isKnownOp(op) {
|
|
28
|
+
return op === "close" || op === "evictUser";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 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 ?
|
|
32
|
+
*
|
|
33
|
+
* C'est ce qu'émet un serveur d'une version plus récente pendant un
|
|
34
|
+
* déploiement progressif (ADR-0025, « Règle de lecture du bus ») : l'ordre
|
|
35
|
+
* n'est pas appliqué, mais il ne se compte pas comme une injection. Rien
|
|
36
|
+
* d'autre que `op` n'est lu — ses autres champs appartiennent à une version
|
|
37
|
+
* qui n'est pas celle-ci. Une charge sans `op` textuel, ou d'un type CONNU de
|
|
38
|
+
* forme fausse, n'est pas « inconnue » : elle est invalide.
|
|
39
|
+
*
|
|
40
|
+
* @param payload - la charge du message reçu (forme inconnue).
|
|
41
|
+
* @returns `true` si le type d'ordre est inconnu de cette version.
|
|
42
|
+
*/
|
|
43
|
+
function isUnknownControlOrder(payload) {
|
|
44
|
+
const op = ownField(payload, "op");
|
|
45
|
+
return isBoundedBusText(op) && !isKnownOp(op);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Lit un ordre de contrôle dans une charge venue du bus — `null` si la forme n'est pas celle d'un ordre connu.
|
|
49
|
+
*
|
|
50
|
+
* Une seule garde pour les deux bords : l'émetteur la passe avant de publier
|
|
51
|
+
* (il n'émet jamais ce que ses pairs refuseraient), le récepteur avant
|
|
52
|
+
* d'appliquer. Lecture par champs PROPRES (`ownField`) : une charge dont le
|
|
53
|
+
* prototype porterait `op` ne passe pas. Un champ inconnu est toléré — c'est
|
|
54
|
+
* ce qui laisse une version suivante ajouter un champ sans faire refuser ses
|
|
55
|
+
* ordres pendant un déploiement progressif ; un champ CONNU de forme fausse
|
|
56
|
+
* fait refuser l'ordre entier.
|
|
57
|
+
*
|
|
58
|
+
* @param payload - la charge du message reçu (forme inconnue).
|
|
59
|
+
* @returns l'ordre, ou `null`.
|
|
60
|
+
*/
|
|
61
|
+
function readControlOrder(payload) {
|
|
62
|
+
const channel = ownField(payload, "channel");
|
|
63
|
+
if (!isBoundedBusText(channel)) return null;
|
|
64
|
+
const op = ownField(payload, "op");
|
|
65
|
+
if (!isKnownOp(op)) return null;
|
|
66
|
+
if (op === "close") return {
|
|
67
|
+
op,
|
|
68
|
+
channel
|
|
69
|
+
};
|
|
70
|
+
const user = ownField(payload, "user");
|
|
71
|
+
if (!isBoundedBusText(user)) return null;
|
|
72
|
+
const generation = ownField(payload, "generation");
|
|
73
|
+
if (generation === void 0) return {
|
|
74
|
+
op,
|
|
75
|
+
channel,
|
|
76
|
+
user
|
|
77
|
+
};
|
|
78
|
+
if (typeof generation !== "number" || !Number.isSafeInteger(generation) || generation < 0) return null;
|
|
79
|
+
return {
|
|
80
|
+
op,
|
|
81
|
+
channel,
|
|
82
|
+
user,
|
|
83
|
+
generation
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export { MAX_CONTROL_FIELD_LENGTH, isBoundedBusText, isUnknownControlOrder, readControlOrder };
|