@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
package/README.md
CHANGED
|
@@ -145,6 +145,10 @@ import { registerBackplaneDriver } from "@nodefony/realtime";
|
|
|
145
145
|
registerBackplaneDriver("nats", (config) => new MyNatsBackplane(config));
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
Un bus fourni par l'application (service `realtimeBackplane`) s'arrête à la charge de
|
|
149
|
+
l'application : `await shutdownRealtime(getRealtimeHub(), bus)` dans son `onTerminate`, pour que
|
|
150
|
+
la fin de la présence parte avant la sortie du processus.
|
|
151
|
+
|
|
148
152
|
Détail des réglages : [`docs/configuration.md`](./docs/configuration.md).
|
|
149
153
|
|
|
150
154
|
## API
|
|
@@ -162,6 +166,7 @@ Détail des réglages : [`docs/configuration.md`](./docs/configuration.md).
|
|
|
162
166
|
| `LoopbackBackplane` / `ClusterBackplane` / `RedisBackplane` | classes | Les 3 drivers natifs |
|
|
163
167
|
| `registerBackplaneDriver` / `getBackplaneDriver` / `listBackplaneDrivers` | functions | Registre ouvert de drivers |
|
|
164
168
|
| `IBackplane` | interface | Contrat d'un fond de panier |
|
|
169
|
+
| `shutdownRealtime` / `BACKPLANE_STOP_TIMEOUT_MS` | fn / const | Arrêt propre : fin de la présence annoncée aux pairs, puis arrêt du bus en laissant partir ce qui est émis (borné). Appelé par le module pour le bus qu'il crée ; à appeler soi-même pour un bus fourni (`realtimeBackplane`) |
|
|
165
170
|
| `ContractRealtimeController<C>` | classe | Base d'un contrôleur qui implémente un contrat (`realtimeActions()` complète si le contrat déclare des `serverMethods`, rien sinon) |
|
|
166
171
|
| `getRealtimeHub<C>()` | fonction | Le hub lu par le contrat (`IContractRealtimeHub<C>`) : `publish` d'un service vérifié ; sans paramètre, le hub libre |
|
|
167
172
|
| `ContractRealtimePublish<C>` | type | Le `publish` d'une fabrique de canal, typé par le contrat |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// \0@oxc-project+runtime@0.
|
|
1
|
+
// \0@oxc-project+runtime@0.158.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
package/dist/index.js
CHANGED
|
@@ -16,12 +16,16 @@ import { getBackplaneDriver, listBackplaneDrivers, registerBackplaneDriver } fro
|
|
|
16
16
|
import { ClusterProbeClient, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, mergeClusterHealth, processProbeTransport, setClusterProbeClient } from "./nodefony/src/cluster/ClusterProbeClient.js";
|
|
17
17
|
import { WsConnectionTransport } from "./nodefony/src/transport/WsConnectionTransport.js";
|
|
18
18
|
import { buildOwnHealth, buildRealtimeHealth, createRealtimeAdminApi } from "./nodefony/src/server/RealtimeAdminApi.js";
|
|
19
|
-
import
|
|
19
|
+
import { BACKPLANE_STOP_TIMEOUT_MS, shutdownRealtime } from "./nodefony/src/server/realtimeShutdown.js";
|
|
20
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
20
21
|
import { ANONYMOUS_REALTIME_TOKEN, ANONYMOUS_SUBSCRIBER_IDENTITY } from "./nodefony/src/server/AnonymousRealtimeToken.js";
|
|
21
22
|
import { RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, RealtimePolicy, getDeclaredBroadcastPrefixes } from "./nodefony/decorators/realtimeDecorators.js";
|
|
22
23
|
import { ContractRealtimeController, ContractRealtimeControllerBase, ContractRealtimeControllerStrict } from "./nodefony/src/server/ContractRealtimeController.js";
|
|
23
24
|
import { BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, createSyslogUplinkHandler } from "./nodefony/src/server/syslogUplink.js";
|
|
24
|
-
import {
|
|
25
|
+
import { DEFAULT_PRESENCE_MAX_NAMED, PRESENCE_WINDOW_MS } from "./nodefony/src/server/channelPresence.js";
|
|
26
|
+
import { BackplaneEcho, openBackplaneEnvelope, sealBackplaneEnvelope } from "./nodefony/src/backplane/envelope.js";
|
|
27
|
+
import { BackplanePublishQueue, DEFAULT_MAX_QUEUE_BYTES } from "./nodefony/src/backplane/publishQueue.js";
|
|
28
|
+
import { BackplaneIngress } from "./nodefony/src/backplane/ingress.js";
|
|
25
29
|
import { IDENTITY_CHANGED_EVENT, Module, services, withTimeout } from "nodefony";
|
|
26
30
|
import { getRouteSchema, isFrameworkRouter } from "@nodefony/framework";
|
|
27
31
|
// index.ts
|
|
@@ -80,6 +84,8 @@ let Realtime = class Realtime extends Module {
|
|
|
80
84
|
static critical = false;
|
|
81
85
|
/** Rôle topologique du process, capté à `onCluster` (défaut mono-process). */
|
|
82
86
|
#clusterRole = "MONO";
|
|
87
|
+
/** Bus créé PAR ce module (registre de pilotes) — à lui de l'arrêter ; `null` s'il est fourni par l'application ou absent. */
|
|
88
|
+
#ownedBackplane = null;
|
|
83
89
|
constructor(kernel) {
|
|
84
90
|
super("realtime", kernel, import.meta.url, config);
|
|
85
91
|
kernel.once("onCluster", (role) => {
|
|
@@ -128,6 +134,11 @@ let Realtime = class Realtime extends Module {
|
|
|
128
134
|
getRealtimeHub().refreshIdentity(event.identifier);
|
|
129
135
|
});
|
|
130
136
|
await this.#wireBackplane();
|
|
137
|
+
this.kernel?.once("onTerminate", async () => {
|
|
138
|
+
const stopped = await shutdownRealtime(getRealtimeHub(), this.#ownedBackplane);
|
|
139
|
+
this.#ownedBackplane = null;
|
|
140
|
+
if (!stopped) this.log(`realtime backplane : arrêt non terminé en ${BACKPLANE_STOP_TIMEOUT_MS} ms (bus injoignable ?) — la fin de la présence peut ne pas avoir atteint les autres serveurs, qui oublieront celui-ci au délai d'oubli`, "WARNING");
|
|
141
|
+
});
|
|
131
142
|
this.#wireClusterProbe(this.#clusterRole);
|
|
132
143
|
return this;
|
|
133
144
|
}
|
|
@@ -229,6 +240,7 @@ let Realtime = class Realtime extends Module {
|
|
|
229
240
|
return;
|
|
230
241
|
}
|
|
231
242
|
hub.setBackplane(backplane);
|
|
243
|
+
this.#ownedBackplane = backplane;
|
|
232
244
|
const info = backplane.describe();
|
|
233
245
|
this.log(`realtime backplane driver=${info.driver} kind=${info.kind} origin=${info.originId} cross-pod=${info.crossPod ? "yes" : "no"}` + (info.channel ? ` channel=${info.channel}` : ""), "INFO");
|
|
234
246
|
this.#logBroadcastPrefixes(hub);
|
|
@@ -287,4 +299,4 @@ let Realtime = class Realtime extends Module {
|
|
|
287
299
|
};
|
|
288
300
|
Realtime = __decorate([services([RealtimeService])], Realtime);
|
|
289
301
|
var realtime_default = Realtime;
|
|
290
|
-
export { ANONYMOUS_REALTIME_TOKEN, ANONYMOUS_SUBSCRIBER_IDENTITY, BROWSER_ORIGIN, ChannelFrame, ClusterBackplane, ClusterProbeClient, ContractRealtimeController, ContractRealtimeControllerBase, ContractRealtimeControllerStrict, LoopbackBackplane, MAX_CLIENT_SEVERITY, REDIS_RT_CHANNEL, RESERVED_SYSTEM_PREFIXES, Realtime, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeController, RealtimeHub, RealtimeInbound, RealtimePolicy, RealtimeService, RedisBackplane, SLOW_CONSUMER_BYTES, ServerRealtimeSocket, WsConnectionTransport, buildOwnHealth, buildRealtimeHealth, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, createRealtimeAdminApi, createRedisServiceTransport, createSyslogUplinkHandler, realtime_default as default, defineRealtimeConfig, getBackplaneDriver, getDeclaredBroadcastPrefixes, getRealtimeHub, isReservedSystemChannel, listBackplaneDrivers, mergeClusterHealth, openBackplaneEnvelope, processIpcTransport, processProbeTransport, realtimeConfigJsonSchema, registerBackplaneDriver, resolveBackplaneOriginId, resolveRedisBytesChannel, resolveRedisChannel, sealBackplaneEnvelope, serverSocket, setClusterProbeClient };
|
|
302
|
+
export { ANONYMOUS_REALTIME_TOKEN, ANONYMOUS_SUBSCRIBER_IDENTITY, BACKPLANE_STOP_TIMEOUT_MS, BROWSER_ORIGIN, BackplaneEcho, BackplaneIngress, BackplanePublishQueue, ChannelFrame, ClusterBackplane, ClusterProbeClient, ContractRealtimeController, ContractRealtimeControllerBase, ContractRealtimeControllerStrict, DEFAULT_MAX_QUEUE_BYTES, DEFAULT_PRESENCE_MAX_NAMED, LoopbackBackplane, MAX_CLIENT_SEVERITY, PRESENCE_WINDOW_MS, REDIS_RT_CHANNEL, RESERVED_SYSTEM_PREFIXES, Realtime, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeController, RealtimeHub, RealtimeInbound, RealtimePolicy, RealtimeService, RedisBackplane, SLOW_CONSUMER_BYTES, ServerRealtimeSocket, WsConnectionTransport, buildOwnHealth, buildRealtimeHealth, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, createRealtimeAdminApi, createRedisServiceTransport, createSyslogUplinkHandler, realtime_default as default, defineRealtimeConfig, getBackplaneDriver, getDeclaredBroadcastPrefixes, getRealtimeHub, isReservedSystemChannel, listBackplaneDrivers, mergeClusterHealth, openBackplaneEnvelope, processIpcTransport, processProbeTransport, realtimeConfigJsonSchema, registerBackplaneDriver, resolveBackplaneOriginId, resolveRedisBytesChannel, resolveRedisChannel, sealBackplaneEnvelope, serverSocket, setClusterProbeClient, shutdownRealtime };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isCallable, isRecord } from "../src/guards.js";
|
|
2
|
-
import {
|
|
2
|
+
import { presenceDefect, samePresence } from "../src/server/channelPresence.js";
|
|
3
|
+
import { BootConfigurationError, Injector, channelPatternWeight, compileChannelPattern } from "nodefony";
|
|
3
4
|
import "reflect-metadata";
|
|
4
5
|
// nodefony/decorators/realtimeDecorators.ts
|
|
5
6
|
/** Clés de métadonnées posées sur le constructor de la classe controller. */
|
|
@@ -7,6 +8,10 @@ const ACTIONS_KEY = "realtime:actions";
|
|
|
7
8
|
const CHANNELS_KEY = "realtime:channels";
|
|
8
9
|
const INBOUND_KEY = "realtime:inbound";
|
|
9
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";
|
|
10
15
|
/**
|
|
11
16
|
* Lit une métadonnée de classe posée par ces décorateurs : un objet nom → valeur,
|
|
12
17
|
* ou `undefined` quand la classe n'en porte pas. Les valeurs restent `unknown` :
|
|
@@ -17,6 +22,11 @@ function readMetadataMap(key, ctor) {
|
|
|
17
22
|
const raw = Reflect.getMetadata(key, ctor);
|
|
18
23
|
return isRecord(raw) ? raw : void 0;
|
|
19
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
|
+
}
|
|
20
30
|
/** Vrai si la valeur est un tableau de chaînes. */
|
|
21
31
|
function isStringArray(value) {
|
|
22
32
|
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
@@ -30,7 +40,8 @@ const POLICY_KEYS = /* @__PURE__ */ new Set([
|
|
|
30
40
|
"roles",
|
|
31
41
|
"scopes",
|
|
32
42
|
"allow",
|
|
33
|
-
"anonymous"
|
|
43
|
+
"anonymous",
|
|
44
|
+
"presence"
|
|
34
45
|
]);
|
|
35
46
|
/**
|
|
36
47
|
* `allow` désigne-t-il une classe d'admission (`@injectable`) plutôt qu'une flèche ?
|
|
@@ -54,7 +65,9 @@ function isAdmissionClass(allow) {
|
|
|
54
65
|
function channelPolicyDefect(value) {
|
|
55
66
|
if (!isRecord(value)) return "la politique n'est pas un objet";
|
|
56
67
|
for (const key of Object.keys(value)) if (!POLICY_KEYS.has(key)) return `clé inconnue « ${key} » (clés admises : ${[...POLICY_KEYS].join(", ")})`;
|
|
57
|
-
const { authenticated, roles, scopes, allow, anonymous } = value;
|
|
68
|
+
const { authenticated, roles, scopes, allow, anonymous, presence } = value;
|
|
69
|
+
const presenceIssue = presenceDefect(presence);
|
|
70
|
+
if (presenceIssue !== null) return presenceIssue;
|
|
58
71
|
if (authenticated !== void 0 && typeof authenticated !== "boolean") return "« authenticated » doit être un booléen";
|
|
59
72
|
if (roles !== void 0 && !isStringArray(roles)) return "« roles » doit être un tableau de chaînes";
|
|
60
73
|
if (scopes !== void 0 && !isStringArray(scopes)) return "« scopes » doit être un tableau de chaînes";
|
|
@@ -66,6 +79,34 @@ function channelPolicyDefect(value) {
|
|
|
66
79
|
if (!Injector.isInjectable(allow) && isRecord(proto) && typeof proto.allow === "function") return `« allow » est une classe (${allow.name}) sans @injectable`;
|
|
67
80
|
return null;
|
|
68
81
|
}
|
|
82
|
+
/** Une valeur lue sous la métadonnée des politiques est-elle une politique recevable ? */
|
|
83
|
+
function isChannelPolicy(value) {
|
|
84
|
+
return channelPolicyDefect(value) === null;
|
|
85
|
+
}
|
|
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;
|
|
93
|
+
return true;
|
|
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
|
+
}
|
|
69
110
|
/**
|
|
70
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.
|
|
71
112
|
*
|
|
@@ -89,12 +130,30 @@ function hasDefinedKey(policy) {
|
|
|
89
130
|
}
|
|
90
131
|
/** Libération d'un canal dont la fabrique décorée n'en a rendu aucune. */
|
|
91
132
|
const NO_DISPOSE = () => {};
|
|
92
|
-
/**
|
|
93
|
-
|
|
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) {
|
|
94
137
|
if (!policy || !hasDefinedKey(policy)) return;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
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);
|
|
98
157
|
}
|
|
99
158
|
/**
|
|
100
159
|
* Politique appliquée à une action RPC dont l'auteur n'a rien déclaré :
|
|
@@ -103,11 +162,6 @@ function definePolicy(ctor, name, policy) {
|
|
|
103
162
|
* Pour une action délibérément publique, l'écrire : `{ authenticated: false }`.
|
|
104
163
|
*/
|
|
105
164
|
const DEFAULT_ACTION_POLICY = Object.freeze({ authenticated: true });
|
|
106
|
-
/** Une politique est-elle DÉJÀ déclarée sous ce nom ? */
|
|
107
|
-
function hasPolicy(ctor, name) {
|
|
108
|
-
const map = readMetadataMap(POLICIES_KEY, ctor);
|
|
109
|
-
return map ? Object.prototype.hasOwnProperty.call(map, name) : false;
|
|
110
|
-
}
|
|
111
165
|
/**
|
|
112
166
|
* Décorateur **méthode** — expose une **action RPC** request→response du
|
|
113
167
|
* controller realtime sous le nom `method` (ex. `"nodefony:kernel:ping"`, `"chat:send"`).
|
|
@@ -127,9 +181,12 @@ function hasPolicy(ctor, name) {
|
|
|
127
181
|
* défaut, toute action applicative serait publique sans que rien ne le dise.
|
|
128
182
|
* Pour une action délibérément ouverte, l'écrire : `{ authenticated: false }`.
|
|
129
183
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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.
|
|
133
190
|
*
|
|
134
191
|
* ⚠️ Le NOM peut primer sur ta politique : le plancher système de
|
|
135
192
|
* `@nodefony/security` réserve les préfixes plateforme (`kernel:`, `syslog:`…)
|
|
@@ -159,8 +216,7 @@ function RealtimeAction(method, policy) {
|
|
|
159
216
|
const map = { ...readMetadataMap(ACTIONS_KEY, ctor) };
|
|
160
217
|
map[method] = propertyKey;
|
|
161
218
|
Reflect.defineMetadata(ACTIONS_KEY, map, ctor);
|
|
162
|
-
|
|
163
|
-
else if (!hasPolicy(ctor, method)) definePolicy(ctor, method, DEFAULT_ACTION_POLICY);
|
|
219
|
+
declarePolicy(ctor, method, "write", policy);
|
|
164
220
|
};
|
|
165
221
|
}
|
|
166
222
|
/**
|
|
@@ -178,6 +234,10 @@ function RealtimeAction(method, policy) {
|
|
|
178
234
|
*
|
|
179
235
|
* Un nom qui porte `*` est un motif (famille de canaux), comme partout ailleurs.
|
|
180
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
|
+
*
|
|
181
241
|
* **Sur un contrôleur de contrat** (`extends ContractRealtimeController<C>`), le
|
|
182
242
|
* décorateur lit le contrat sur la classe qu'il décore : chaque nom doit être
|
|
183
243
|
* une méthode ou un canal de `C`, sinon la classe ne compile pas, et l'erreur
|
|
@@ -193,20 +253,25 @@ function RealtimeAction(method, policy) {
|
|
|
193
253
|
*/
|
|
194
254
|
function RealtimePolicy(policies) {
|
|
195
255
|
return function(target) {
|
|
196
|
-
for (const [name, policy] of Object.entries(policies))
|
|
256
|
+
for (const [name, policy] of Object.entries(policies)) {
|
|
257
|
+
declarePolicy(target, name, "read", policy);
|
|
258
|
+
declarePolicy(target, name, "write", policy);
|
|
259
|
+
}
|
|
197
260
|
};
|
|
198
261
|
}
|
|
199
262
|
/**
|
|
200
|
-
* Décorateur **méthode** — expose un **canal pub/sub** sous le nom `channel
|
|
201
|
-
*
|
|
202
|
-
* 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
|
|
203
266
|
* désabonné, comme avec l'override classique).
|
|
204
267
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
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.
|
|
210
275
|
*
|
|
211
276
|
* Le `this` est lié à l'instance ; le factory peut donc lire `this.kernel`, etc.
|
|
212
277
|
* Mais ATTENTION : un provider est **partagé** entre toutes les connexions au
|
|
@@ -214,8 +279,20 @@ function RealtimePolicy(policies) {
|
|
|
214
279
|
* créé. Capturer des **valeurs long-lived** (broker, syslog) dans la closure,
|
|
215
280
|
* jamais `this.context` (lié à la connexion créatrice qui peut fermer).
|
|
216
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
|
+
*
|
|
217
288
|
* @example
|
|
218
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
|
+
*
|
|
219
296
|
* @RealtimeChannel("chat:room42")
|
|
220
297
|
* room42(_channel: string, publish: RealtimePublish): () => void {
|
|
221
298
|
* const timer = setInterval(() => publish("chat:room42", { ts: Date.now() }), 1000);
|
|
@@ -229,7 +306,7 @@ function RealtimeChannel(channel, policy) {
|
|
|
229
306
|
const map = { ...readMetadataMap(CHANNELS_KEY, ctor) };
|
|
230
307
|
map[channel] = propertyKey;
|
|
231
308
|
Reflect.defineMetadata(CHANNELS_KEY, map, ctor);
|
|
232
|
-
|
|
309
|
+
declarePolicy(ctor, channel, "read", policy);
|
|
233
310
|
};
|
|
234
311
|
}
|
|
235
312
|
/**
|
|
@@ -244,6 +321,20 @@ function RealtimeChannel(channel, policy) {
|
|
|
244
321
|
* pousser tant qu'aucun handler n'est enregistré ici). Cf seam P13.8a
|
|
245
322
|
* (`beforeDispatch`) pour la gate sécurité par voter P6.
|
|
246
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
|
+
*
|
|
247
338
|
* @example
|
|
248
339
|
* class ChatController extends RealtimeController {
|
|
249
340
|
* @RealtimeInbound("chat:send")
|
|
@@ -260,7 +351,7 @@ function RealtimeInbound(method, policy) {
|
|
|
260
351
|
const map = { ...readMetadataMap(INBOUND_KEY, ctor) };
|
|
261
352
|
map[method] = propertyKey;
|
|
262
353
|
Reflect.defineMetadata(INBOUND_KEY, map, ctor);
|
|
263
|
-
|
|
354
|
+
declarePolicy(ctor, method, "write", policy);
|
|
264
355
|
};
|
|
265
356
|
}
|
|
266
357
|
/**
|
|
@@ -301,6 +392,47 @@ function getRealtimeChannels(instance) {
|
|
|
301
392
|
}
|
|
302
393
|
return out;
|
|
303
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
|
+
}
|
|
304
436
|
/** Lecture (lazy, au handshake) du registre des canaux inbound décorés. */
|
|
305
437
|
function getRealtimeInbound(instance) {
|
|
306
438
|
const map = readMetadataMap(INBOUND_KEY, instance.constructor);
|
|
@@ -323,19 +455,79 @@ function declaresAction(ctor, name) {
|
|
|
323
455
|
return map !== void 0 && Object.prototype.hasOwnProperty.call(map, name);
|
|
324
456
|
}
|
|
325
457
|
/**
|
|
326
|
-
* Politiques
|
|
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}.
|
|
327
522
|
*
|
|
328
523
|
* @param ctor - constructeur du contrôleur.
|
|
329
|
-
* @returns nom de canal ou de méthode → politique, ou `null` si la
|
|
330
|
-
* déclare aucune
|
|
331
|
-
* @throws BootConfigurationError Une politique est illisible
|
|
332
|
-
* canal et le défaut sont nommés.
|
|
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.
|
|
333
528
|
*/
|
|
334
529
|
function getChannelPoliciesOf(ctor) {
|
|
335
|
-
|
|
336
|
-
if (map === void 0) return null;
|
|
337
|
-
assertPolicyMap(typeof ctor === "function" ? ctor.name : "(contrôleur)", map);
|
|
338
|
-
return map;
|
|
530
|
+
return getDeclaredChannelPolicies(ctor)?.read ?? null;
|
|
339
531
|
}
|
|
340
532
|
/**
|
|
341
533
|
* Politiques déclarées sur la classe d'une instance de contrôleur — même lecture que {@link getChannelPoliciesOf}.
|
|
@@ -393,4 +585,4 @@ function getDeclaredBroadcastPrefixes() {
|
|
|
393
585
|
function applyDeclaredBroadcastPrefixes(hub) {
|
|
394
586
|
for (const prefix of declaredBroadcastPrefixes) hub.markBroadcastChannel(prefix);
|
|
395
587
|
}
|
|
396
|
-
export { DEFAULT_ACTION_POLICY, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, RealtimePolicy, applyDeclaredBroadcastPrefixes, channelPolicyDefect, declaresAction, getChannelPoliciesOf, getDeclaredBroadcastPrefixes, getRealtimeActions, getRealtimeChannelPolicies, getRealtimeChannels, getRealtimeInbound, isAdmissionClass };
|
|
588
|
+
export { DEFAULT_ACTION_POLICY, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, RealtimePolicy, applyDeclaredBroadcastPrefixes, channelPolicyDefect, declaresAction, getChannelPoliciesOf, getDeclaredBroadcastPrefixes, getDeclaredChannelPolicies, getRealtimeActions, getRealtimeChannelPolicies, getRealtimeChannels, getRealtimeInbound, isAdmissionClass, patternChannelFactory, samePolicy };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import resolveBackplaneOriginId from "./originId.js";
|
|
2
2
|
import { isCallable, isRecord, isThenable } from "../guards.js";
|
|
3
|
-
import {
|
|
3
|
+
import { sealBackplaneEnvelope, sealBackplaneEnvelopeBytes } from "./envelope.js";
|
|
4
|
+
import { BackplaneIngress } from "./ingress.js";
|
|
4
5
|
import { BackplanePublishQueue } from "./publishQueue.js";
|
|
5
6
|
import { containsBytes } from "nodefony";
|
|
6
7
|
// nodefony/src/backplane/RedisBackplane.ts
|
|
@@ -167,8 +168,8 @@ var RedisBackplane = class RedisBackplane {
|
|
|
167
168
|
/** Borne mémoire des publications en vol (cf {@link BackplanePublishQueue}). */
|
|
168
169
|
#queue;
|
|
169
170
|
#notice;
|
|
170
|
-
/**
|
|
171
|
-
#
|
|
171
|
+
/** Tri de l'entrée : écho, sceau, refus comptés (brique commune aux pilotes). */
|
|
172
|
+
#ingress;
|
|
172
173
|
#handler = null;
|
|
173
174
|
#started = false;
|
|
174
175
|
/**
|
|
@@ -183,6 +184,7 @@ var RedisBackplane = class RedisBackplane {
|
|
|
183
184
|
this.#secret = secret;
|
|
184
185
|
this.#notice = options.onNotice ?? null;
|
|
185
186
|
this.#queue = new BackplanePublishQueue(options.maxQueueBytes ?? 8388608, this.#notice);
|
|
187
|
+
this.#ingress = new BackplaneIngress(originId, secret, this.#notice);
|
|
186
188
|
}
|
|
187
189
|
/**
|
|
188
190
|
* Abonne au canal Redis dédié. Idempotent (le flag est posé avant l'`await` →
|
|
@@ -191,7 +193,7 @@ var RedisBackplane = class RedisBackplane {
|
|
|
191
193
|
async start() {
|
|
192
194
|
if (this.#started) return;
|
|
193
195
|
this.#started = true;
|
|
194
|
-
await Promise.all([this.#transport.subscribe(this.#redisChannel, (raw) => this.#
|
|
196
|
+
await Promise.all([this.#transport.subscribe(this.#redisChannel, (raw) => this.#receive(raw)), this.#subscribeBytes()]);
|
|
195
197
|
}
|
|
196
198
|
/**
|
|
197
199
|
* Arme le sujet binaire (ADR-0021 S6) : les charges qui contiennent des octets
|
|
@@ -216,7 +218,7 @@ var RedisBackplane = class RedisBackplane {
|
|
|
216
218
|
async #subscribeBytes() {
|
|
217
219
|
const bytes = this.#bytes;
|
|
218
220
|
if (bytes === null) return;
|
|
219
|
-
await this.#transport.subscribeBytes?.(bytes.channel, (raw) => this.#
|
|
221
|
+
await this.#transport.subscribeBytes?.(bytes.channel, (raw) => this.#receiveBytes(raw, bytes.codec));
|
|
220
222
|
}
|
|
221
223
|
/**
|
|
222
224
|
* Propage une publication locale aux autres pods via Redis. NE refait PAS le
|
|
@@ -225,7 +227,8 @@ var RedisBackplane = class RedisBackplane {
|
|
|
225
227
|
* L'envoi passe par la **file bornée** : si le bus n'acquitte plus, les
|
|
226
228
|
* publications suivantes sont jetées et comptées plutôt que d'empiler des
|
|
227
229
|
* mégaoctets dans le client Redis (sémantique at-most-once déjà assumée par le
|
|
228
|
-
* 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}).
|
|
229
232
|
*/
|
|
230
233
|
publish(channel, payload) {
|
|
231
234
|
const env = {
|
|
@@ -236,55 +239,44 @@ var RedisBackplane = class RedisBackplane {
|
|
|
236
239
|
const bytes = this.#bytes;
|
|
237
240
|
if (bytes !== null && containsBytes(payload)) {
|
|
238
241
|
const bin = sealBackplaneEnvelopeBytes(env, bytes.codec, this.#secret);
|
|
239
|
-
this.#queue.send(bin.byteLength, () => this.#transport.publish(bytes.channel, bin));
|
|
242
|
+
this.#queue.send(channel, bin.byteLength, () => this.#transport.publish(bytes.channel, bin));
|
|
240
243
|
return;
|
|
241
244
|
}
|
|
242
245
|
const raw = sealBackplaneEnvelope(env, this.#secret);
|
|
243
|
-
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));
|
|
244
247
|
}
|
|
245
248
|
/** Un seul handler d'ingress ; un appel ultérieur remplace le précédent. */
|
|
246
249
|
onMessage(handler) {
|
|
247
250
|
this.#handler = handler;
|
|
248
251
|
}
|
|
249
252
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* première fois puis une fois sur cent (ADR-0021 S6) : une rotation du secret ou
|
|
255
|
-
* un producteur mal configuré ne perd plus de messages en silence.
|
|
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).
|
|
256
257
|
*/
|
|
257
|
-
#
|
|
258
|
-
const msg =
|
|
259
|
-
if (msg
|
|
260
|
-
if (this.#secret !== null) this.#sealRejected();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (msg.originId === this.originId) return;
|
|
264
|
-
this.#handler?.(msg);
|
|
265
|
-
}
|
|
266
|
-
/** Même tri que {@link #ingress}, sur le sujet binaire : décodage, sceau, anti-echo. */
|
|
267
|
-
#ingressBytes(raw, codec) {
|
|
268
|
-
const msg = openBackplaneEnvelopeBytes(raw, codec, this.#secret);
|
|
269
|
-
if (msg === null) {
|
|
270
|
-
if (this.#secret !== null) this.#sealRejected();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (msg.originId === this.originId) return;
|
|
274
|
-
this.#handler?.(msg);
|
|
258
|
+
#receive(raw) {
|
|
259
|
+
const msg = this.#ingress.text(raw);
|
|
260
|
+
if (msg !== null) this.#handler?.(msg);
|
|
275
261
|
}
|
|
276
|
-
/**
|
|
277
|
-
#
|
|
278
|
-
this.#
|
|
279
|
-
|
|
280
|
-
if (n !== 1 && n % 100 !== 0) return;
|
|
281
|
-
this.#notice?.(`backplane : ${n} message(s) reçu(s) au sceau refusé, JETÉS. Causes : rotation du secret en cours (des exemplaires n'ont pas le même NF_REALTIME_BACKPLANE_SECRET), producteur mal configuré, ou écriture tierce dans le bus. Compteur : sealRejectedTotal.`, "WARNING");
|
|
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);
|
|
282
266
|
}
|
|
283
|
-
/**
|
|
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
|
+
*/
|
|
284
275
|
async stop() {
|
|
285
276
|
this.#handler = null;
|
|
286
277
|
if (!this.#started) return;
|
|
287
278
|
this.#started = false;
|
|
279
|
+
await this.#queue.settled();
|
|
288
280
|
await Promise.all([this.#transport.unsubscribe(this.#redisChannel), this.#bytes !== null ? this.#transport.unsubscribe(this.#bytes.channel) : void 0]);
|
|
289
281
|
}
|
|
290
282
|
describe() {
|
|
@@ -295,7 +287,8 @@ var RedisBackplane = class RedisBackplane {
|
|
|
295
287
|
crossPod: true,
|
|
296
288
|
channel: this.#redisChannel,
|
|
297
289
|
sealed: this.#secret !== null,
|
|
298
|
-
...this.#secret !== null && { sealRejectedTotal: this.#sealRejectedTotal },
|
|
290
|
+
...this.#secret !== null && { sealRejectedTotal: this.#ingress.sealRejectedTotal },
|
|
291
|
+
echoTotal: this.#ingress.echoTotal,
|
|
299
292
|
queue: this.#queue.describe(),
|
|
300
293
|
...this.#bytes !== null && { bytes: true }
|
|
301
294
|
};
|