@nodefony/realtime 10.0.0-beta.3 → 10.0.0-beta.5

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 (51) hide show
  1. package/README.md +29 -10
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +4 -6
  4. package/dist/nodefony/config/config.js +1 -2
  5. package/dist/nodefony/config/configSchema.js +1 -2
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  7. package/dist/nodefony/decorators/realtimeDecorators.js +1 -2
  8. package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
  9. package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
  10. package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
  11. package/dist/nodefony/src/backplane/RedisBackplane.js +1 -2
  12. package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
  13. package/dist/nodefony/src/backplane/bootNotices.js +1 -2
  14. package/dist/nodefony/src/backplane/envelope.js +1 -2
  15. package/dist/nodefony/src/backplane/originId.js +1 -2
  16. package/dist/nodefony/src/backplane/publishQueue.js +0 -1
  17. package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -2
  18. package/dist/nodefony/src/guards.js +1 -2
  19. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -2
  20. package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
  21. package/dist/nodefony/src/server/RealtimeController.js +158 -84
  22. package/dist/nodefony/src/server/RealtimeHub.js +3 -3
  23. package/dist/nodefony/src/server/ServerRealtimeSocket.js +18 -9
  24. package/dist/nodefony/src/server/deniedDetail.js +1 -2
  25. package/dist/nodefony/src/server/subscribedAck.js +29 -0
  26. package/dist/nodefony/src/server/syslogUplink.js +1 -2
  27. package/dist/nodefony/src/server/welcomeEnv.js +1 -2
  28. package/dist/nodefony/src/service/RealtimeService.js +1 -2
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
  30. package/dist/nodefony/testing/index.js +77 -4
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
  33. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
  34. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  35. package/dist/types/nodefony/src/server/RealtimeController.d.ts +60 -2
  36. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
  37. package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
  38. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
  39. package/dist/types/nodefony/testing/index.d.ts +11 -4
  40. package/docs/actions.md +67 -44
  41. package/docs/architecture.md +125 -119
  42. package/docs/cadence-et-contre-pression.md +31 -22
  43. package/docs/configuration.md +106 -82
  44. package/docs/cookbook-chat.md +56 -34
  45. package/docs/index.md +35 -24
  46. package/docs/observabilite.md +49 -49
  47. package/docs/protocole.md +91 -68
  48. package/docs/securite.md +135 -115
  49. package/docs/vocabulaire.md +117 -114
  50. package/package.json +10 -10
  51. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,6 +1,7 @@
1
+ import { isRecord } from "../src/guards.js";
1
2
  import { getRealtimeHub } from "../src/server/RealtimeHub.js";
2
3
  import { RequestContext } from "nodefony";
3
- //#region nodefony/testing/index.ts
4
+ // nodefony/testing/index.ts
4
5
  /**
5
6
  * `@nodefony/realtime/testing` — **harnais de test d'un controller temps réel**.
6
7
  *
@@ -53,6 +54,67 @@ async function waitFor(predicate, ticks) {
53
54
  return predicate();
54
55
  }
55
56
  /**
57
+ * Motifs de refus du protocole, rendus exhaustifs par le compilateur : un motif
58
+ * ajouté à `IRealtimeDenied.reason` casse la compilation ICI, au lieu de faire
59
+ * lever le harnais sur un refus légitime.
60
+ */
61
+ const DENIED_REASONS = {
62
+ forbidden: true,
63
+ limit: true,
64
+ unknown: true
65
+ };
66
+ function isDeniedReason(reason) {
67
+ return Object.hasOwn(DENIED_REASONS, reason);
68
+ }
69
+ /** Erreur JSON-RPC 2.0 (§5.1) d'une trame, ou `null` si sa forme est fausse. */
70
+ function readRpcError(error) {
71
+ if (!isRecord(error) || typeof error.code !== "number" || typeof error.message !== "string") return null;
72
+ return Object.hasOwn(error, "data") ? {
73
+ code: error.code,
74
+ message: error.message,
75
+ data: error.data
76
+ } : {
77
+ code: error.code,
78
+ message: error.message
79
+ };
80
+ }
81
+ /**
82
+ * Lit une trame émise par le contrôleur sous test. Une trame dont un champ a
83
+ * la mauvaise forme LÈVE : c'est un défaut du contrôleur, que le test doit
84
+ * voir, pas une donnée à écarter.
85
+ */
86
+ function readFrame(data) {
87
+ const value = JSON.parse(data);
88
+ const fault = () => /* @__PURE__ */ new Error(`createRealtimeHarness: trame JSON-RPC malformée émise — ${data}`);
89
+ if (!isRecord(value)) throw fault();
90
+ const { jsonrpc, id, method } = value;
91
+ if (jsonrpc !== void 0 && typeof jsonrpc !== "string") throw fault();
92
+ if (id !== void 0 && typeof id !== "number" && typeof id !== "string") throw fault();
93
+ if (method !== void 0 && typeof method !== "string") throw fault();
94
+ const error = Object.hasOwn(value, "error") ? readRpcError(value.error) : void 0;
95
+ if (error === null) throw fault();
96
+ return {
97
+ ...jsonrpc === void 0 ? {} : { jsonrpc },
98
+ ...id === void 0 ? {} : { id },
99
+ ...method === void 0 ? {} : { method },
100
+ ...Object.hasOwn(value, "params") ? { params: value.params } : {},
101
+ ...Object.hasOwn(value, "result") ? { result: value.result } : {},
102
+ ...error === void 0 ? {} : { error }
103
+ };
104
+ }
105
+ /** Lit le `params` d'un `realtime:denied` — lève si le contrôleur l'a mal formé. */
106
+ function readDenied(params) {
107
+ if (isRecord(params) && typeof params.channel === "string" && typeof params.reason === "string" && isDeniedReason(params.reason) && (params.detail === void 0 || typeof params.detail === "string")) return params.detail === void 0 ? {
108
+ channel: params.channel,
109
+ reason: params.reason
110
+ } : {
111
+ channel: params.channel,
112
+ reason: params.reason,
113
+ detail: params.detail
114
+ };
115
+ throw new Error(`createRealtimeHarness: realtime:denied malformé — ${JSON.stringify(params)}`);
116
+ }
117
+ /**
56
118
  * Monte un controller temps réel sur une fausse connexion et rend de quoi le
57
119
  * piloter.
58
120
  *
@@ -85,7 +147,7 @@ function createRealtimeHarness(create, options = {}) {
85
147
  const connection = {
86
148
  readyState: OPEN,
87
149
  send: (data, cb) => {
88
- received.push(JSON.parse(data));
150
+ received.push(readFrame(data));
89
151
  cb?.();
90
152
  },
91
153
  close: (code, reason) => {
@@ -96,6 +158,7 @@ function createRealtimeHarness(create, options = {}) {
96
158
  }
97
159
  };
98
160
  const controller = create({
161
+ type: "websocket",
99
162
  connection,
100
163
  once: (event, fn) => {
101
164
  if (event === "onFinish") onFinish = fn;
@@ -147,10 +210,21 @@ function createRealtimeHarness(create, options = {}) {
147
210
  return welcome;
148
211
  },
149
212
  async subscribe(channel) {
213
+ const from = received.length;
150
214
  await feed(rpc({
151
215
  method: "subscribe",
152
216
  params: { channel }
153
217
  }));
218
+ await waitFor(() => {
219
+ for (let i = from; i < received.length; i++) {
220
+ const f = received[i];
221
+ if ((f?.method === "realtime:subscribed" || f?.method === "realtime:denied") && isRecord(f.params) && f.params["channel"] === channel) return true;
222
+ }
223
+ return false;
224
+ }, 20);
225
+ },
226
+ subscriptions() {
227
+ return received.filter((f) => f.method === "realtime:subscribed").map((f) => isRecord(f.params) ? String(f.params["channel"]) : "");
154
228
  },
155
229
  async unsubscribe(channel) {
156
230
  await feed(rpc({
@@ -187,7 +261,7 @@ function createRealtimeHarness(create, options = {}) {
187
261
  return received.filter((f) => f.method === channel && f.id === void 0).map((f) => f.params);
188
262
  },
189
263
  denials() {
190
- return received.filter((f) => f.method === "realtime:denied").map((f) => f.params);
264
+ return received.filter((f) => f.method === "realtime:denied").map((f) => readDenied(f.params));
191
265
  },
192
266
  close() {
193
267
  if (closed) return;
@@ -201,5 +275,4 @@ function createRealtimeHarness(create, options = {}) {
201
275
  }
202
276
  };
203
277
  }
204
- //#endregion
205
278
  export { createRealtimeHarness };
@@ -81,6 +81,7 @@ export { RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, ge
81
81
  export type { RealtimeChannelFactory } from "./nodefony/decorators/realtimeDecorators.js";
82
82
  export type { IRealtimeController, RealtimePublish, RealtimeInboundHandler, } from "./nodefony/interfaces/IRealtimeController.js";
83
83
  export type { IChannelPolicy } from "./nodefony/interfaces/IChannelPolicy.js";
84
+ export type { IRealtimeConnectionState } from "./nodefony/interfaces/IRealtimeConnectionState.js";
84
85
  export type { IClientLogsLimits } from "./nodefony/interfaces/IClientLogsLimits.js";
85
86
  export { createSyslogUplinkHandler, BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, } from "./nodefony/src/server/syslogUplink.js";
86
87
  export type { SyslogUplinkHandlerOptions, ClientSeverity, } from "./nodefony/src/server/syslogUplink.js";
@@ -0,0 +1,40 @@
1
+ import type { JsonRpcPeer } from "nodefony";
2
+ import type { WsConnectionTransport } from "../src/transport/WsConnectionTransport.js";
3
+ import type { ChannelSink } from "../src/server/RealtimeHub.js";
4
+ import type { RealtimeInboundHandler } from "./IRealtimeController.js";
5
+ /**
6
+ * État du protocole temps réel d'UNE connexion WebSocket — posé au welcome, et
7
+ * relu à chaque frame jusqu'à la fermeture.
8
+ *
9
+ * Il vit sur la CONNEXION (`WebsocketContext`), pas sur le contrôleur : un
10
+ * `forward` fait construire une instance neuve qui sert la même socket, et
11
+ * l'état doit lui survivre.
12
+ *
13
+ * Le pair y est rangé sous son contrat EFFACÉ, ce qu'il est d'ailleurs à sa
14
+ * construction. Plusieurs contrôleurs peuvent servir la même connexion, et aucun
15
+ * type ne relie cet état à l'un d'eux : chacun le relit sous son propre contrat
16
+ * (`RealtimeController.requestClient` → `JsonRpcPeer.requestAs`).
17
+ */
18
+ export interface IRealtimeConnectionState {
19
+ /** `realtime:welcome` émis : la poignée de main ne se rejoue pas. */
20
+ welcomed: boolean;
21
+ /** Le pair JSON-RPC de la connexion, sous son contrat effacé. */
22
+ peer: JsonRpcPeer;
23
+ /** Le transport qui relie le pair à la socket (contre-pression, sonde). */
24
+ transport: WsConnectionTransport;
25
+ /** Canal → sink de CETTE connexion auprès du hub partagé, pour s'en désabonner. */
26
+ channels: Map<string, ChannelSink>;
27
+ /** Canaux full-duplex qui acceptent une entrée client ; `null` s'il n'y en a aucun. */
28
+ inbound: Record<string, RealtimeInboundHandler> | null;
29
+ }
30
+ declare module "@nodefony/http" {
31
+ interface IWebsocketConnectionState {
32
+ /**
33
+ * État du protocole temps réel de la connexion, posé par
34
+ * `@nodefony/realtime` au welcome ; `undefined` avant, et sur toute socket
35
+ * qu'aucun `RealtimeController` ne sert. Réservé au module : le lire,
36
+ * jamais l'écrire.
37
+ */
38
+ __nfRealtime?: IRealtimeConnectionState;
39
+ }
40
+ }
@@ -45,4 +45,17 @@ export interface IRealtimeToken {
45
45
  * (publish/subscribe), payé seulement sur `api.request`.
46
46
  */
47
47
  isValid?(nowMs?: number): boolean | Promise<boolean>;
48
+ /**
49
+ * Le COMPTE qui a ouvert la socket existe-t-il toujours, actif, et est-ce
50
+ * bien le même ? `false` si le compte a disparu, a été désactivé ou
51
+ * verrouillé, ou si un AUTRE compte a repris son identifiant (suppression
52
+ * hors framework puis recréation). Optionnel — absent = considéré confirmé.
53
+ *
54
+ * Distinct d'{@link isValid} parce que son coût l'est : il relit l'annuaire
55
+ * des comptes (une lecture, comme une requête HTTP en zone), là où
56
+ * `isValid` ne lit qu'un store de jetons ou de sessions. Appelé par le pont
57
+ * `api.request` SEULEMENT, jamais par la re-validation périodique du hub —
58
+ * qui le paierait pour chaque socket ouverte, à chaque tour.
59
+ */
60
+ confirmAccount?(): Promise<boolean>;
48
61
  }
@@ -11,6 +11,7 @@
11
11
  export type { IBackplane, IBackplaneMessage, BackplaneHandler, } from "./IBackplane.js";
12
12
  export type { IRealtimeController, RealtimePublish, RealtimeInboundHandler, } from "./IRealtimeController.js";
13
13
  export type { IChannelPolicy } from "./IChannelPolicy.js";
14
+ export type { IRealtimeConnectionState } from "./IRealtimeConnectionState.js";
14
15
  export type { IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, IRealtimeChannelStat, IRealtimeConnProbe, } from "./IRealtimeProbe.js";
15
16
  export type { IRealtimeToken } from "./IRealtimeToken.js";
16
17
  export type { IRealtimeHandshake } from "./IRealtimeHandshake.js";
@@ -26,6 +26,7 @@ import type { IRealtimeController, RealtimePublish, RealtimeInboundHandler } fro
26
26
  * d'action n'est payé que sur une requête `id`. C'est aussi le seam du backplane Redis.
27
27
  */
28
28
  export declare abstract class RealtimeController<Emit extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> extends Controller implements IRealtimeController {
29
+ #private;
29
30
  /**
30
31
  * Une instance PAR CONNEXION, par construction — le défaut `singleton` des
31
32
  * contrôleurs ne s'applique pas ici. Le pair JSON-RPC de la connexion capture
@@ -90,7 +91,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
90
91
  */
91
92
  protected realtimeInbound(): Record<string, RealtimeInboundHandler>;
92
93
  /**
93
- * Opt-in du **pont API** (« API souveraine », POC Ph.3) : `true` → la
94
+ * Opt-in du **pont API** (une action, deux transports) : `true` → la
94
95
  * connexion expose la méthode RPC `api.request {path}` qui re-route le path
95
96
  * porté par le message vers l'action controller correspondante — la MÊME
96
97
  * action que le GET REST (`socket.request("/nodefony/kernel/api/modules")`
@@ -120,6 +121,39 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
120
121
  * écoute (welcome) » de `NodefonySocketCoverage.test.ts`.
121
122
  */
122
123
  protected handleRealtime(message: string | Buffer | null): void;
124
+ /**
125
+ * Lit les trames BINAIRES de cette connexion — le point d'extension d'un
126
+ * protocole binaire écrit par l'application, à côté du JSON-RPC ; pendant
127
+ * serveur de `NodefonySocket.onBinary`.
128
+ *
129
+ * Le protocole temps réel est du JSON-RPC texte : il ne sait pas à quel canal
130
+ * une trame binaire appartient. Sans cette méthode, une trame binaire est
131
+ * écartée, comptée (`binaryDiscarded` de la sonde de connexion) et journalisée
132
+ * une fois par connexion. La surcharger la reçoit telle quelle, octets exacts,
133
+ * en `Uint8Array` — le type que livre aussi `NodefonySocket.onBinary` dans le
134
+ * navigateur : un décodeur s'écrit une fois pour les deux côtés. À l'exécution
135
+ * c'est un `Buffer` de `ws`, qui en est un.
136
+ *
137
+ * ⚠️ Hors du verrou de trame, et pour toute la série 10 : la trame brute ne
138
+ * passe ni par l'autorisation `beforeDispatch` ni par les gardes d'action et de
139
+ * canal du JSON-RPC. La connexion, elle, a déjà franchi Origin, l'authentificateur
140
+ * et le pare-feu. L'identité se lit par `RequestContext.getAuthenticatedUser()`,
141
+ * et toute décision d'accès sur une trame binaire appartient à l'application.
142
+ *
143
+ * ⚠️ Un `DataView` se construit sur `data.buffer` AVEC `data.byteOffset` et
144
+ * `data.byteLength` : un `Buffer` issu du pool de Node ne commence pas à 0.
145
+ *
146
+ * @param data - les octets reçus.
147
+ */
148
+ protected onRealtimeBinary?(data: Uint8Array): void;
149
+ /**
150
+ * Faute de câblage : `handleRealtime` reçu par une instance qui ne sert pas
151
+ * une connexion WebSocket — route sans `methods: ["WEBSOCKET"]`, ou faux
152
+ * contexte de test sans `type`. Levée plutôt que passée sous silence : le
153
+ * client attendrait un welcome qui ne vient jamais. Chemin froid.
154
+ */
155
+ private outsideWebsocketError;
156
+ private receiveBinary;
123
157
  /**
124
158
  * **L1 — duplex serveur→client.** Émet une REQUÊTE vers le client de CETTE
125
159
  * connexion (sur une action qu'il a exposée via `client.register(...)`) et attend
@@ -143,6 +177,22 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
143
177
  * handshake n'est pas terminé.
144
178
  */
145
179
  protected notifyClient<K extends EventNames<Emit>>(method: K, params?: EventPayload<Emit, K>): void;
180
+ /**
181
+ * Émet une trame BINAIRE vers CETTE connexion, sans la sérialiser — pendant
182
+ * sortant de {@link onRealtimeBinary}, et de `NodefonySocket.sendBinary` côté
183
+ * client. Même contre-pression que les frames JSON-RPC.
184
+ *
185
+ * @param data - les octets à émettre (`Uint8Array`, donc un `Buffer` aussi).
186
+ * @returns `false` si le handshake n'est pas terminé, la connexion fermée ou
187
+ * la trame jetée par la contre-pression.
188
+ */
189
+ protected sendBinaryToClient(data: Uint8Array): boolean;
190
+ /**
191
+ * État realtime de CETTE connexion, `undefined` avant le welcome — pour les
192
+ * chemins FROIDS (requête sortante, binaire). `notifyClient` et la réception
193
+ * gardent leur lecture en ligne : ils sont sur le chemin de chaque frame.
194
+ */
195
+ private coldConnState;
146
196
  /**
147
197
  * Handshake : seams sécurité #4 (Origin RFC 6455 §10.2) + #2 (authenticator
148
198
  * réseau) PUIS crée peer+transport, enregistre les actions, welcome + cleanup.
@@ -164,6 +214,14 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
164
214
  * canaux FULL-DUPLEX déclarés (entrée client → handler `realtimeInbound`).
165
215
  */
166
216
  private onRealtimeNotification;
217
+ /**
218
+ * Acquitte un abonnement ACCEPTÉ (`realtime:subscribed`) — le seul signal
219
+ * qui dise au client à partir de quand le flux du canal lui est livré.
220
+ *
221
+ * Un refus n'y passe jamais : il a `realtime:denied`. Cold path (1×/canal et
222
+ * par connexion).
223
+ */
224
+ private acknowledgeSubscription;
167
225
  /** Abonne la connexion à un canal via le hub partagé (idempotent par connexion). */
168
226
  private startChannel;
169
227
  /**
@@ -187,7 +245,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
187
245
  */
188
246
  private actionParamsWrapper;
189
247
  /**
190
- * Handler de la méthode RPC `api.request {path}` — le **pont API souverain**
248
+ * Handler de la méthode RPC `api.request {path}` — le **pont socket**
191
249
  * (POC Ph.3) : résout le path porté par le message via le Router
192
250
  * (`cleanPathOverride` — l'URL de la connexion n'est jamais mutée) puis
193
251
  * exécute l'action SANS la rendre (`executeAction`) : la valeur nue est
@@ -16,8 +16,9 @@ import { type IRealtimeSocket, type IRealtimeChannel, type IChannelStats, type R
16
16
  * l'état créé est passif, et c'est la fabrique du controller qui tranchera quand
17
17
  * une connexion cliente le demandera. Un service n'invente donc jamais un canal
18
18
  * pour les autres.
19
- * - `request` : **non supporté** — un handle au-dessus du hub n'a PAS de pair unique
20
- * (le hub est multi-clients). Pour un RPC serveur→client 1-1, utiliser
19
+ * - `request` : **non supporté, et déprécié** — un handle au-dessus du hub n'a PAS de
20
+ * pair unique (le hub est multi-clients), et le contrat commun `IRealtimeSocket` ne
21
+ * le promet plus. Pour un RPC serveur→client 1-1, utiliser
21
22
  * `RealtimeController.requestClient` (par connexion, L1).
22
23
  *
23
24
  * Lazy : aucune structure allouée tant que le service n'`on`/`subscribe`/`publish` pas.
@@ -25,7 +26,7 @@ import { type IRealtimeSocket, type IRealtimeChannel, type IChannelStats, type R
25
26
  * @typeParam Emit — canaux pub/sub SORTANTS (`publish`) = ServerToClient.
26
27
  * @typeParam Listen — canaux pub/sub RÉCEPTIONNÉS (`subscribe`/`on`) = ClientToServer.
27
28
  */
28
- export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEventsMap, Listen extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> implements IRealtimeSocket<Emit, Listen, Actions> {
29
+ export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEventsMap, Listen extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> implements IRealtimeSocket<Emit, Listen> {
29
30
  #private;
30
31
  private readonly hub;
31
32
  constructor(hub?: import("./RealtimeHub.js").RealtimeHub);
@@ -39,14 +40,23 @@ export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEvents
39
40
  subscribe(channel: EventNames<Listen> | (string & {})): void;
40
41
  /** Désabonne (ref-compté) : coupe le sink du hub au DERNIER consommateur. */
41
42
  unsubscribe(channel: EventNames<Listen> | (string & {})): void;
42
- /** Branche un handler de réception sur un canal. Renvoie un `dispose`. */
43
+ /**
44
+ * Branche un handler de réception sur un canal. Renvoie un `dispose`.
45
+ *
46
+ * Le handler typé par `Listen` se range sans conversion sous
47
+ * `RealtimeHandler`, qui porte la frontière du contrat typé : il reçoit ce
48
+ * que le hub publie, dont `Listen` décrit la forme sans la valider.
49
+ */
43
50
  on<K extends string>(channel: K, handler: K extends EventNames<Listen> ? (payload: EventPayload<Listen, K>) => void : RealtimeHandler): () => void;
44
- /** Retire un handler d'un canal. */
51
+ /** Retire un handler d'un canal — même frontière que {@link on}. */
45
52
  off<K extends string>(channel: K, handler: K extends EventNames<Listen> ? (payload: EventPayload<Listen, K>) => void : RealtimeHandler): void;
46
53
  /**
47
- * **Non supporté.** Un handle au-dessus du hub n'a pas de pair unique
48
- * (multi-clients). Pour un RPC serveur→client 1-1, utiliser
49
- * `RealtimeController.requestClient` (par connexion, L1).
54
+ * **Non supporté** : rejette toujours. Un handle au-dessus du hub n'a pas de pair
55
+ * unique (multi-clients), et le contrat commun `IRealtimeSocket` ne promet plus
56
+ * `request`. Comportement inchangé pour toute la série 10.
57
+ *
58
+ * @deprecated Retrait en 11.0. Pour un RPC serveur→client 1-1, utiliser
59
+ * `RealtimeController.requestClient` (par connexion, L1).
50
60
  */
51
61
  request<K extends string, T = unknown>(_method: K, _params?: ContractParams<Actions, K>, _timeoutMs?: number): Promise<ContractResult<Actions, K, T>>;
52
62
  /** Vue par-canal ({@link IRealtimeChannel}) — fine liaison sur les primitives. */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Trame d'acquittement d'abonnement — `realtime:subscribed`, écrite d'un bloc.
3
+ *
4
+ * Tout ce qui précède le nom du canal ne dépend que du protocole : il est
5
+ * calculé une fois au chargement du module. Seul le canal se sérialise par
6
+ * trame. On évite ainsi l'enveloppe allouée et le `JSON.stringify` générique
7
+ * de `JsonRpcPeer.notify` — mesuré à ~0,5 µs par acquittement, soit l'essentiel
8
+ * du surcoût d'un `subscribe` acquitté.
9
+ *
10
+ * ⚠️ C'est une SECONDE écriture du format de notification : son égalité avec
11
+ * `JsonRpcPeer.buildNotification` est verrouillée par `subscribedAck.test.ts`,
12
+ * sur des noms de canal hostiles (guillemets, barres obliques inverses,
13
+ * caractères de contrôle, Unicode).
14
+ *
15
+ * @internal
16
+ */
17
+ /**
18
+ * Sérialise la notification `realtime:subscribed { channel }`.
19
+ *
20
+ * @param channel - le canal acquitté (entrée réseau : échappé par `JSON.stringify`).
21
+ * @returns la trame JSON-RPC 2.0 prête à écrire sur le transport.
22
+ */
23
+ export declare function subscribedAckFrame(channel: string): string;
@@ -6,7 +6,7 @@ import type { IRealtimeConnProbe } from "../../interfaces/IRealtimeProbe.js";
6
6
  * `ctx.connection` (WebsocketContext) la fournit.
7
7
  */
8
8
  export interface RawWsConnection {
9
- send(data: string, cb?: (err?: Error) => void): void;
9
+ send(data: string | ArrayBuffer | ArrayBufferView, cb?: (err?: Error) => void): void;
10
10
  close(code?: number, reason?: string): void;
11
11
  readonly readyState: number;
12
12
  /**
@@ -46,6 +46,8 @@ export declare class WsConnectionTransport implements IRealtimeTransport, IRealt
46
46
  private _bytesSent;
47
47
  private _messagesSent;
48
48
  private _dropped;
49
+ private _binaryDiscarded;
50
+ private _malformedDiscarded;
49
51
  private readonly _max;
50
52
  private readonly _policy;
51
53
  private readonly _closeAfterDrops;
@@ -56,6 +58,29 @@ export declare class WsConnectionTransport implements IRealtimeTransport, IRealt
56
58
  });
57
59
  connect(): void;
58
60
  send(raw: string): void;
61
+ /**
62
+ * Émet des octets tels quels — trame binaire RFC 6455, hors JSON-RPC (#617).
63
+ * Même contre-pression (`decideSend`, la règle unique) et mêmes compteurs que
64
+ * {@link send}. Écrite à part, et non en factorisant `send` : `send` est le
65
+ * chemin de CHAQUE frame JSON-RPC, et n'y paie pas un appel de plus.
66
+ *
67
+ * @param data - les octets à émettre.
68
+ * @returns `true` si la trame est partie, `false` si la connexion était
69
+ * fermée ou la contre-pression l'a jetée.
70
+ */
71
+ sendBinary(data: Uint8Array): boolean;
72
+ /**
73
+ * Compte une trame ENTRANTE écartée et rend le cumul de sa nature — `1` dit
74
+ * « première », le seul instant où l'appelant journalise.
75
+ *
76
+ * @param kind - `binary` (sans lecteur) ou `malformed` (texte non JSON).
77
+ * @returns le cumul de cette nature, après incrément.
78
+ */
79
+ discard(kind: "binary" | "malformed"): number;
80
+ /** Cumul de trames BINAIRES reçues et écartées faute de lecteur — monotone. */
81
+ get binaryDiscarded(): number;
82
+ /** Cumul de trames TEXTE reçues et écartées parce que non JSON — monotone. */
83
+ get malformedDiscarded(): number;
59
84
  close(code?: number, reason?: string): void;
60
85
  get readyState(): number;
61
86
  /** Octets en file d'envoi non drainés (`ws.bufferedAmount`) — risque #1. `0` si mock. */
@@ -88,9 +88,11 @@ export interface IRealtimeHarnessOptions {
88
88
  /**
89
89
  * Verrou de frame (seam #1). Neutre par construction : `@nodefony/realtime`
90
90
  * ne connaît pas `@nodefony/security`. Une application qui charge la sécurité
91
- * passe ici son `buildFrameAuthorizer(...)` pour éprouver une politique de
92
- * canal ou une action gardée par rôle — sans lui, une `policy` déclarée n'est
93
- * appliquée par personne, exactement comme au runtime.
91
+ * passe ici `createTestFrameAuthorizer(getRealtimeHub())` de
92
+ * `@nodefony/security/testing` — le verrou du pare-feu, jamais une copie —
93
+ * pour éprouver une politique de canal ou une action gardée par rôle. Sans
94
+ * lui, une `policy` déclarée n'est appliquée par personne, exactement comme
95
+ * au runtime.
94
96
  */
95
97
  readonly frameAuthorizer?: FrameAuthorizer;
96
98
  /**
@@ -130,7 +132,10 @@ export interface IRealtimeHarness<C extends RealtimeController> {
130
132
  * échec) : lire `closes` pour en connaître la cause.
131
133
  */
132
134
  connect(): Promise<IRealtimeFrame>;
133
- /** Demande un canal, comme le ferait un client. */
135
+ /**
136
+ * Demande un canal, comme le ferait un client, et attend la réponse du
137
+ * serveur (acquittement `realtime:subscribed` ou refus `realtime:denied`).
138
+ */
134
139
  subscribe(channel: string): Promise<void>;
135
140
  /** Rend un canal — le provider est disposé au dernier abonné. */
136
141
  unsubscribe(channel: string): Promise<void>;
@@ -149,6 +154,8 @@ export interface IRealtimeHarness<C extends RealtimeController> {
149
154
  messages(channel: string): readonly unknown[];
150
155
  /** Les refus d'abonnement notifiés (`realtime:denied`). */
151
156
  denials(): readonly IHarnessDenied[];
157
+ /** Les canaux dont l'abonnement a été acquitté (`realtime:subscribed`), dans l'ordre. */
158
+ subscriptions(): readonly string[];
152
159
  /** Ferme la socket côté client : la base dispose alors tous ses providers. */
153
160
  close(): void;
154
161
  /** `close()` puis remise à zéro du hub. À appeler en fin de cas. */