@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.
Files changed (96) hide show
  1. package/README.md +55 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +91 -12
  4. package/dist/nodefony/config/config.js +7 -0
  5. package/dist/nodefony/decorators/realtimeDecorators.js +341 -43
  6. package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
  7. package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
  8. package/dist/nodefony/src/backplane/RedisBackplane.js +146 -41
  9. package/dist/nodefony/src/backplane/controlOrder.js +86 -0
  10. package/dist/nodefony/src/backplane/envelope.js +505 -27
  11. package/dist/nodefony/src/backplane/ingress.js +93 -0
  12. package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
  13. package/dist/nodefony/src/backplane/publishQueue.js +97 -3
  14. package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
  15. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
  16. package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
  17. package/dist/nodefony/src/server/RealtimeConnection.js +135 -0
  18. package/dist/nodefony/src/server/RealtimeController.js +409 -107
  19. package/dist/nodefony/src/server/RealtimeHub.js +1158 -111
  20. package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
  21. package/dist/nodefony/src/server/admissionBudget.js +176 -0
  22. package/dist/nodefony/src/server/channelPolicyRegistry.js +95 -0
  23. package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
  24. package/dist/nodefony/src/server/channelPresence.js +488 -0
  25. package/dist/nodefony/src/server/connectionAdmission.js +242 -0
  26. package/dist/nodefony/src/server/contractRoutes.js +75 -0
  27. package/dist/nodefony/src/server/deniedDetail.js +10 -3
  28. package/dist/nodefony/src/server/frameAdmission.js +160 -0
  29. package/dist/nodefony/src/server/presenceBus.js +486 -0
  30. package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
  31. package/dist/nodefony/src/server/remotePresence.js +541 -0
  32. package/dist/nodefony/src/server/subscribedAck.js +13 -1
  33. package/dist/nodefony/src/server/syslogUplink.js +3 -4
  34. package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
  35. package/dist/nodefony/src/service/RealtimeService.js +51 -1
  36. package/dist/nodefony/src/service/publicOrigin.js +40 -0
  37. package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
  38. package/dist/nodefony/testing/backplaneConformance.js +347 -0
  39. package/dist/nodefony/testing/index.js +43 -12
  40. package/dist/nodefony/testing/presenceClient.js +54 -0
  41. package/dist/types/index.d.ts +39 -7
  42. package/dist/types/nodefony/config/config.d.ts +9 -0
  43. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +198 -17
  44. package/dist/types/nodefony/interfaces/IBackplane.d.ts +46 -51
  45. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
  46. package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
  47. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
  48. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
  49. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
  50. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  52. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
  53. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +89 -18
  54. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +75 -0
  55. package/dist/types/nodefony/src/backplane/envelope.d.ts +77 -0
  56. package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
  57. package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
  58. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
  59. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
  60. package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
  61. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +20 -0
  62. package/dist/types/nodefony/src/server/RealtimeController.d.ts +110 -12
  63. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +448 -42
  64. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
  65. package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
  66. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +41 -0
  67. package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
  68. package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
  69. package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
  70. package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
  71. package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
  72. package/dist/types/nodefony/src/server/frameAdmission.d.ts +66 -0
  73. package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
  74. package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
  75. package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
  76. package/dist/types/nodefony/src/server/subscribedAck.d.ts +10 -0
  77. package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
  78. package/dist/types/nodefony/src/service/RealtimeService.d.ts +17 -2
  79. package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
  80. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
  81. package/dist/types/nodefony/testing/backplaneConformance.d.ts +78 -0
  82. package/dist/types/nodefony/testing/index.d.ts +13 -2
  83. package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
  84. package/docs/actions.md +420 -18
  85. package/docs/architecture.md +101 -11
  86. package/docs/cadence-et-contre-pression.md +87 -11
  87. package/docs/canaux-a-membres.md +277 -0
  88. package/docs/cas-d-usage.md +1703 -0
  89. package/docs/configuration.md +175 -19
  90. package/docs/cookbook-chat.md +701 -224
  91. package/docs/index.md +11 -1
  92. package/docs/observabilite.md +97 -21
  93. package/docs/protocole.md +142 -33
  94. package/docs/securite.md +347 -41
  95. package/docs/vocabulaire.md +13 -12
  96. 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.155.0/helpers/esm/decorate.js
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 { REDIS_RT_CHANNEL, RedisBackplane, createRedisServiceTransport, isRedisPublisher, isRedisSubscriber, resolveRedisChannel } from "./nodefony/src/backplane/RedisBackplane.js";
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 __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
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) return;
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.");