@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
package/README.md
CHANGED
|
@@ -95,6 +95,39 @@ Les trois décorateurs prennent `(nom, policy?)` — la politique déclare qui a
|
|
|
95
95
|
|
|
96
96
|
Recette complète (salon, présence, historique, upload) : [`docs/cookbook-chat.md`](./docs/cookbook-chat.md).
|
|
97
97
|
|
|
98
|
+
### Typer les deux bouts — un contrat
|
|
99
|
+
|
|
100
|
+
Un **contrat** déclare UNE fois ce qui circule, dans un fichier de types seuls partagé par le
|
|
101
|
+
navigateur et le serveur ; le compilateur vérifie alors les deux bouts — une action oubliée, une
|
|
102
|
+
charge de forme fausse ou un nom de politique mal orthographié ne compilent plus.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// shared/chat.ts
|
|
106
|
+
import type { IRealtimeContract } from "nodefony/client";
|
|
107
|
+
export interface ChatContract extends IRealtimeContract {
|
|
108
|
+
serverToClient: { "chat:room": { from: string; text: string } };
|
|
109
|
+
serverMethods: { "chat:send": { in: { text: string }; out: { id: string } } };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// navigateur
|
|
113
|
+
const socket = NodefonySocket.fromContract<ChatContract>({
|
|
114
|
+
url: "/chat/realtime",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// serveur
|
|
118
|
+
@RealtimePolicy({ "chat:send": { roles: ["ROLE_USER"] } })
|
|
119
|
+
class ChatRt extends ContractRealtimeController<ChatContract> {
|
|
120
|
+
protected realtimeActions() {
|
|
121
|
+
return {
|
|
122
|
+
"chat:send": (p: { text: string }) => ({ id: String(p.text.length) }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Ce que chaque bout reçoit (`Jsonify`), ce que le compilateur refuse, et le flux SSE typé par la
|
|
129
|
+
même carte : [`docs/actions.md`](./docs/actions.md#-un-contrat-pour-les-deux-bouts).
|
|
130
|
+
|
|
98
131
|
## Backplane
|
|
99
132
|
|
|
100
133
|
Le backplane est le fond de panier qui relie les pods entre eux. Trois drivers sont livrés avec le
|
|
@@ -112,6 +145,10 @@ import { registerBackplaneDriver } from "@nodefony/realtime";
|
|
|
112
145
|
registerBackplaneDriver("nats", (config) => new MyNatsBackplane(config));
|
|
113
146
|
```
|
|
114
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
|
+
|
|
115
152
|
Détail des réglages : [`docs/configuration.md`](./docs/configuration.md).
|
|
116
153
|
|
|
117
154
|
## API
|
|
@@ -129,6 +166,11 @@ Détail des réglages : [`docs/configuration.md`](./docs/configuration.md).
|
|
|
129
166
|
| `LoopbackBackplane` / `ClusterBackplane` / `RedisBackplane` | classes | Les 3 drivers natifs |
|
|
130
167
|
| `registerBackplaneDriver` / `getBackplaneDriver` / `listBackplaneDrivers` | functions | Registre ouvert de drivers |
|
|
131
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`) |
|
|
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) |
|
|
171
|
+
| `getRealtimeHub<C>()` | fonction | Le hub lu par le contrat (`IContractRealtimeHub<C>`) : `publish` d'un service vérifié ; sans paramètre, le hub libre |
|
|
172
|
+
| `ContractRealtimePublish<C>` | type | Le `publish` d'une fabrique de canal, typé par le contrat |
|
|
173
|
+
| `RealtimePolicy` | décorateur | Politiques d'accès par nom, posées sur la classe ; noms vérifiés contre le contrat |
|
|
132
174
|
| `IRealtimeAuthenticator` / `IRealtimeToken` / `IRealtimeHandshake` | interfaces | Seams sécurité du handshake |
|
|
133
175
|
| `ANONYMOUS_REALTIME_TOKEN` | const | Jeton anonyme gelé — fallback Zero Trust |
|
|
134
176
|
|
|
@@ -205,6 +247,19 @@ seul avec un canal ouvert à tous.
|
|
|
205
247
|
| `closes` / `notices` | Fermetures serveur (4003…) · avertissements de plateforme |
|
|
206
248
|
| `close()` / `dispose()` | Ferme la socket · ferme puis remet le hub à zéro |
|
|
207
249
|
|
|
250
|
+
Un canal gardé par une **classe d'admission** (`@RealtimeChannel("room:*", { allow: RoomAdmission })`)
|
|
251
|
+
se résout au démarrage, dans le conteneur du noyau — que le harnais n'a pas. Le test la construit
|
|
252
|
+
avec le MÊME service que le controller, et la passe par `admissions` ; sans elle, `connect()` jette
|
|
253
|
+
(code 1011), comme un démarrage qui ne la trouverait pas :
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const members = new RoomService(createTestModule());
|
|
257
|
+
const alice = createRealtimeHarness((ctx) => new RoomRealtime(ctx, members), {
|
|
258
|
+
identity: createTestIdentity({ identifier: "alice" }),
|
|
259
|
+
admissions: [new RoomAdmission(members)],
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
208
263
|
`nodefony create controller --kind realtime` pose ce test à côté du controller qu'il génère.
|
|
209
264
|
|
|
210
265
|
## Doc complète
|
|
@@ -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
|
@@ -2,23 +2,32 @@ import config from "./nodefony/config/config.js";
|
|
|
2
2
|
import { buildRealtimeConfig, defineRealtimeConfig, realtimeConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
3
3
|
import { isCallable, isRecord } from "./nodefony/src/guards.js";
|
|
4
4
|
import { realtimeBackplaneDownNotice, realtimeLocalOnlyNotice } from "./nodefony/src/backplane/bootNotices.js";
|
|
5
|
-
import resolveBackplaneOriginId from "./nodefony/src/backplane/originId.js";
|
|
6
|
-
import { LoopbackBackplane } from "./nodefony/src/backplane/LoopbackBackplane.js";
|
|
7
|
-
import { ANONYMOUS_REALTIME_TOKEN } from "./nodefony/src/server/AnonymousRealtimeToken.js";
|
|
8
|
-
import { RESERVED_SYSTEM_PREFIXES, RealtimeHub, SLOW_CONSUMER_BYTES, getRealtimeHub, isReservedSystemChannel } from "./nodefony/src/server/RealtimeHub.js";
|
|
9
|
-
import { RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, getDeclaredBroadcastPrefixes } from "./nodefony/decorators/realtimeDecorators.js";
|
|
10
5
|
import { RealtimeService } from "./nodefony/src/service/RealtimeService.js";
|
|
11
|
-
import { BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, createSyslogUplinkHandler } from "./nodefony/src/server/syslogUplink.js";
|
|
12
|
-
import { WsConnectionTransport } from "./nodefony/src/transport/WsConnectionTransport.js";
|
|
13
6
|
import { RealtimeController } from "./nodefony/src/server/RealtimeController.js";
|
|
7
|
+
import { createAdmissionResolver, registerControllerPolicies } from "./nodefony/src/server/channelPolicyRegistry.js";
|
|
8
|
+
import { assertContractRoutes, readContractRoutes } from "./nodefony/src/server/contractRoutes.js";
|
|
14
9
|
import { ServerRealtimeSocket, serverSocket } from "./nodefony/src/server/ServerRealtimeSocket.js";
|
|
15
|
-
import {
|
|
10
|
+
import { ChannelFrame, RESERVED_SYSTEM_PREFIXES, RealtimeHub, SLOW_CONSUMER_BYTES, getRealtimeHub, isReservedSystemChannel } from "./nodefony/src/server/RealtimeHub.js";
|
|
11
|
+
import { LoopbackBackplane } from "./nodefony/src/backplane/LoopbackBackplane.js";
|
|
12
|
+
import { REDIS_RT_CHANNEL, RedisBackplane, createRedisServiceTransport, isRedisBytesSubscriber, isRedisPublisher, isRedisSubscriber, resolveRedisBytesChannel, resolveRedisChannel } from "./nodefony/src/backplane/RedisBackplane.js";
|
|
13
|
+
import resolveBackplaneOriginId from "./nodefony/src/backplane/originId.js";
|
|
16
14
|
import { ClusterBackplane, processIpcTransport } from "./nodefony/src/backplane/ClusterBackplane.js";
|
|
17
15
|
import { getBackplaneDriver, listBackplaneDrivers, registerBackplaneDriver } from "./nodefony/src/backplane/backplaneRegistry.js";
|
|
18
16
|
import { ClusterProbeClient, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, mergeClusterHealth, processProbeTransport, setClusterProbeClient } from "./nodefony/src/cluster/ClusterProbeClient.js";
|
|
17
|
+
import { WsConnectionTransport } from "./nodefony/src/transport/WsConnectionTransport.js";
|
|
19
18
|
import { buildOwnHealth, buildRealtimeHealth, createRealtimeAdminApi } from "./nodefony/src/server/RealtimeAdminApi.js";
|
|
20
|
-
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";
|
|
21
|
+
import { ANONYMOUS_REALTIME_TOKEN, ANONYMOUS_SUBSCRIBER_IDENTITY } from "./nodefony/src/server/AnonymousRealtimeToken.js";
|
|
22
|
+
import { RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, RealtimePolicy, getDeclaredBroadcastPrefixes } from "./nodefony/decorators/realtimeDecorators.js";
|
|
23
|
+
import { ContractRealtimeController, ContractRealtimeControllerBase, ContractRealtimeControllerStrict } from "./nodefony/src/server/ContractRealtimeController.js";
|
|
24
|
+
import { BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, createSyslogUplinkHandler } from "./nodefony/src/server/syslogUplink.js";
|
|
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";
|
|
21
29
|
import { IDENTITY_CHANGED_EVENT, Module, services, withTimeout } from "nodefony";
|
|
30
|
+
import { getRouteSchema, isFrameworkRouter } from "@nodefony/framework";
|
|
22
31
|
// index.ts
|
|
23
32
|
/**
|
|
24
33
|
* @nodefony/realtime — Couche realtime serveur Nodefony.
|
|
@@ -65,7 +74,7 @@ registerBackplaneDriver(RedisBackplane.driver, (ctx) => {
|
|
|
65
74
|
const namespace = ctx.config.backplane.namespace ?? ctx.module.kernel?.projectName;
|
|
66
75
|
const secret = ctx.config.backplane.secret ?? null;
|
|
67
76
|
if (!secret) ctx.module.log(`driver "${RedisBackplane.driver}" : bus NON AUTHENTIFIÉ — les messages ne sont pas scellés. Quiconque écrit dans ce Redis publie sur les canaux de tous les pods. Poser backplane.secret (ou NF_REALTIME_BACKPLANE_SECRET, ≥ 32 caractères, identique sur tous les pods) pour sceller le transport.`, "WARNING");
|
|
68
|
-
return new RedisBackplane(createRedisServiceTransport(publisher, subscriber), ctx.originId, resolveRedisChannel(namespace), secret, {
|
|
77
|
+
return new RedisBackplane(createRedisServiceTransport(publisher, subscriber, isRedisBytesSubscriber(subscriber) ? subscriber : void 0), ctx.originId, resolveRedisChannel(namespace), secret, {
|
|
69
78
|
maxQueueBytes: ctx.config.backplane.maxQueueBytes,
|
|
70
79
|
onNotice: (message, severity) => ctx.module.log(message, severity)
|
|
71
80
|
});
|
|
@@ -75,6 +84,8 @@ let Realtime = class Realtime extends Module {
|
|
|
75
84
|
static critical = false;
|
|
76
85
|
/** Rôle topologique du process, capté à `onCluster` (défaut mono-process). */
|
|
77
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;
|
|
78
89
|
constructor(kernel) {
|
|
79
90
|
super("realtime", kernel, import.meta.url, config);
|
|
80
91
|
kernel.once("onCluster", (role) => {
|
|
@@ -123,10 +134,64 @@ let Realtime = class Realtime extends Module {
|
|
|
123
134
|
getRealtimeHub().refreshIdentity(event.identifier);
|
|
124
135
|
});
|
|
125
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
|
+
});
|
|
126
142
|
this.#wireClusterProbe(this.#clusterRole);
|
|
127
143
|
return this;
|
|
128
144
|
}
|
|
129
145
|
/**
|
|
146
|
+
* Phase `onReady` — inscrit au hub les politiques de canal de TOUS les contrôleurs temps réel connus, et résout leurs classes d'admission, avant que le moindre serveur n'écoute.
|
|
147
|
+
*
|
|
148
|
+
* Pas à `onKernelBoot` : chaque module enregistre ses contrôleurs dans SON
|
|
149
|
+
* `onBoot` (`@controllers`), et ceux d'un module listé après celui-ci n'y
|
|
150
|
+
* seraient pas encore. `onReady` passe après tous les `onBoot` et avant
|
|
151
|
+
* `initServers` : aucune poignée de main ne précède l'inscription, et un
|
|
152
|
+
* exemplaire qui n'émet que des webhooks a ses politiques comme les autres.
|
|
153
|
+
*
|
|
154
|
+
* @throws BootConfigurationError Politique illisible, `allow` sur une
|
|
155
|
+
* action, classe d'admission sans singleton, sans `allow` ou dont une
|
|
156
|
+
* dépendance manque — la classe et le canal sont nommés.
|
|
157
|
+
*/
|
|
158
|
+
async onKernelReady() {
|
|
159
|
+
if (!this.config.enabled) return this;
|
|
160
|
+
const hub = getRealtimeHub();
|
|
161
|
+
const container = this.kernel?.container;
|
|
162
|
+
const resolveAdmission = createAdmissionResolver((key) => key === null ? void 0 : container?.get(key), (message) => this.log(message, "INFO"));
|
|
163
|
+
const controllers = Module.controllers;
|
|
164
|
+
let mounted = null;
|
|
165
|
+
for (const key in controllers) {
|
|
166
|
+
const ctor = controllers[key];
|
|
167
|
+
if (ctor !== void 0 && ctor.prototype instanceof RealtimeController) {
|
|
168
|
+
registerControllerPolicies(hub, ctor, resolveAdmission);
|
|
169
|
+
const table = readContractRoutes(ctor);
|
|
170
|
+
if (table !== null) {
|
|
171
|
+
mounted ??= this.mountedRoutes();
|
|
172
|
+
assertContractRoutes(ctor.name, table, mounted);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Les routes montées par le routeur du framework, vues par le contrôle des
|
|
180
|
+
* routes de contrat : chemin, méthodes, schémas d'entrée.
|
|
181
|
+
*
|
|
182
|
+
* @returns la liste, vide si le routeur du framework est absent.
|
|
183
|
+
*/
|
|
184
|
+
mountedRoutes() {
|
|
185
|
+
const router = this.kernel?.container?.get("router");
|
|
186
|
+
if (!isFrameworkRouter(router)) return [];
|
|
187
|
+
const all = router.getRoutes("");
|
|
188
|
+
return (Array.isArray(all) ? all : []).flatMap((route) => route.path === void 0 ? [] : [{
|
|
189
|
+
path: route.path,
|
|
190
|
+
methods: route.methodsSet,
|
|
191
|
+
schema: route.controller !== void 0 && route.classMethod !== void 0 ? getRouteSchema(route.controller.prototype, route.classMethod) : null
|
|
192
|
+
}]);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
130
195
|
* Sélectionne et branche le backplane via le **registre de drivers** — résout
|
|
131
196
|
* `config.backplane.driver` (chaîne) → fabrique, SANS connaître aucun nom de
|
|
132
197
|
* driver en dur (pas de chaîne de `if`). La fabrique construit l'instance (ou
|
|
@@ -146,7 +211,10 @@ let Realtime = class Realtime extends Module {
|
|
|
146
211
|
async #wireBackplane() {
|
|
147
212
|
const config = this.config;
|
|
148
213
|
const hub = getRealtimeHub();
|
|
149
|
-
if (hub.backplane !== null)
|
|
214
|
+
if (hub.backplane !== null) {
|
|
215
|
+
this.#logBroadcastPrefixes(hub);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
150
218
|
const driverName = config.backplane.driver;
|
|
151
219
|
const factory = getBackplaneDriver(driverName);
|
|
152
220
|
if (!factory) {
|
|
@@ -172,8 +240,19 @@ let Realtime = class Realtime extends Module {
|
|
|
172
240
|
return;
|
|
173
241
|
}
|
|
174
242
|
hub.setBackplane(backplane);
|
|
243
|
+
this.#ownedBackplane = backplane;
|
|
175
244
|
const info = backplane.describe();
|
|
176
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");
|
|
246
|
+
this.#logBroadcastPrefixes(hub);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Dit au démarrage QUELS canaux traversent le bus : les autres restent sur ce
|
|
250
|
+
* serveur, et un abonné d'un autre serveur n'en reçoit rien — sans erreur.
|
|
251
|
+
* Lu une fois, à côté de la ligne du backplane (D5, ADR-0022 §6).
|
|
252
|
+
*/
|
|
253
|
+
#logBroadcastPrefixes(hub) {
|
|
254
|
+
const prefixes = hub.broadcastPrefixes();
|
|
255
|
+
this.log(`realtime diffusion entre serveurs : ${prefixes.map((p) => `${p}*`).join(", ")} — tout autre canal reste sur ce serveur (\`@RealtimeBroadcast\` pour le diffuser ; oublis comptés par la sonde, \`localOnlyTotal\`)`, "INFO");
|
|
177
256
|
}
|
|
178
257
|
/**
|
|
179
258
|
* `backplane.start()` borné par un **timeout** — un driver réseau (Redis/Kafka)
|
|
@@ -220,4 +299,4 @@ let Realtime = class Realtime extends Module {
|
|
|
220
299
|
};
|
|
221
300
|
Realtime = __decorate([services([RealtimeService])], Realtime);
|
|
222
301
|
var realtime_default = Realtime;
|
|
223
|
-
export { ANONYMOUS_REALTIME_TOKEN, BROWSER_ORIGIN, ClusterBackplane, ClusterProbeClient, LoopbackBackplane, MAX_CLIENT_SEVERITY, REDIS_RT_CHANNEL, RESERVED_SYSTEM_PREFIXES, Realtime, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeController, RealtimeHub, RealtimeInbound, 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, processIpcTransport, processProbeTransport, realtimeConfigJsonSchema, registerBackplaneDriver, resolveBackplaneOriginId, resolveRedisChannel, 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,3 +1,4 @@
|
|
|
1
|
+
import { realtimeConnectSource } from "../src/service/publicOrigin.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
// nodefony/config/config.ts
|
|
3
4
|
/**
|
|
@@ -50,10 +51,16 @@ const clientLogsSchema = z.strictObject({
|
|
|
50
51
|
const csrfSchema = z.strictObject({ checkOrigin: checkOriginSchema.default(() => checkOriginSchema.parse({})) }).describe("Protections CSRF natives realtime (origin check upgrade WS).");
|
|
51
52
|
const realtimeConfigSchema = z.strictObject({
|
|
52
53
|
enabled: z.boolean().default(true).describe("Active le module realtime au boot. Recommandation prod : true. false = module chargé mais inerte (registry, mais aucun hub/listener actif)."),
|
|
54
|
+
codec: z.enum(["json", "cbor"]).default("json").describe("Codec binaire des frames, accordé aux clients qui l'offrent (ADR-0022 §2). `json` (défaut) : JSON-RPC texte seul, le fil d'avant les codecs. `cbor` : le serveur charge le codec CBOR (RFC 8949) au démarrage et l'accorde à tout client qui offre `nodefony.jsonrpc.cbor.v1` ; les autres restent en texte, avec la convention d'octets `$bytes`. Les octets d'une charge voyagent alors sans base64, et toute trame binaire d'une connexion CBOR est une frame : le flux brut (`onRealtimeBinary`, `sendBinaryToClient`) n'y sert plus. Env : `NF_REALTIME_CODEC` — `NF_REALTIME_CODEC=json` remet le fil en texte pour le lire dans le navigateur, une exception de débogage : en texte, la taille des trames et la contre-pression changent.").meta({ env: "NF_REALTIME_CODEC" }),
|
|
53
55
|
backplane: backplaneSchema.default(() => backplaneSchema.parse({})),
|
|
54
56
|
cluster: clusterSchema.default(() => clusterSchema.parse({})),
|
|
55
57
|
slowConsumer: slowConsumerSchema.default(() => slowConsumerSchema.parse({})),
|
|
56
58
|
limits: limitsSchema.default(() => limitsSchema.parse({})),
|
|
59
|
+
authenticateTimeoutMs: z.number().int().positive().max(2147483647).default(1e4).describe("Délai (ms) de l'authentification à la poignée de main WebSocket. Dépassé, la connexion est fermée en 1013 (« try again later ») : le client recule puis revient, et une identité arrivée après coup est ignorée (`authenticateTimedOutTotal`). Jamais 4001 : un refus définitif arrêterait un client légitime à chaque panne de base. Défaut 10000 : l'authentification relit souvent une session en base ou une clé distante ; 10 s couvrent une base chargée, et au-delà retenir la connexion ne l'authentifierait pas plus vite — elle occuperait seulement une place."),
|
|
60
|
+
allowTimeoutMs: z.number().int().positive().max(2147483647).default(5e3).describe("Délai (ms) de la règle d'admission `allow` d'un canal, compté depuis la réception du `subscribe`, attente en file comprise. Dépassé, l'abonné reçoit `realtime:denied` motif `unavailable` et réessaie ; un verdict arrivé après coup est ignoré, jamais une admission. Le minuteur n'est armé que si `allow` rend une promesse. Défaut 5000 : une lecture en base saine répond en millisecondes, 5 s couvrent une base chargée sans laisser un écran vide au-delà de ce qu'un humain attend."),
|
|
61
|
+
maxPendingAllowsPerConnection: z.number().int().positive().default(8).describe("Appels de `allow` en vol pour UNE connexion. Au-delà, la demande d'abonnement ATTEND son tour (jamais refusée : le rejeu au `welcome` renvoie tous les canaux d'un coup). Défaut 8 : assez pour ouvrir plusieurs canaux en parallèle, assez peu pour qu'une connexion ne monopolise pas le pool de connexions à la base (souvent 10)."),
|
|
62
|
+
maxPendingAllowsTotal: z.number().int().positive().default(512).describe("Appels de `allow` en vol pour TOUT l'exemplaire. Au-delà, la demande reçoit `unavailable` sans appel (`allowSaturatedTotal`). Défaut 512 : à ce niveau la base est déjà l'étranglement ; empiler davantage d'appels n'admettrait personne plus vite et retarderait la reprise."),
|
|
63
|
+
publicOrigin: z.string().refine((value) => realtimeConnectSource(value) !== null, { message: "publicOrigin : une origine nue est attendue — schéma ws, wss, http ou https, hôte, port facultatif ; ni chemin, ni joker (ex. `wss://rt.example.com`)." }).optional().describe("Origine où la socket est servie quand ce n'est PAS celle de la page (autre domaine, autre port), par ex. `wss://rt.example.com`. Le module la déclare lui-même dans `connect-src` de la politique de contenu ; sans elle, le navigateur bloque la connexion. Inutile quand la socket est sur l'origine de la page (cas courant) : `connect-src 'self'` la couvre déjà. `https://` est accepté et déclaré en `wss://`. Ne dit pas qui peut OUVRIR la socket : c'est `csrf.checkOrigin.allowList`. Env : `NF_REALTIME_PUBLIC_ORIGIN` (l'origine distingue des déploiements).").meta({ env: "NF_REALTIME_PUBLIC_ORIGIN" }),
|
|
57
64
|
csrf: csrfSchema.default(() => csrfSchema.parse({})),
|
|
58
65
|
clientLogs: clientLogsSchema.default(() => clientLogsSchema.parse({}))
|
|
59
66
|
}).describe("Configuration de @nodefony/realtime.");
|