@nodefony/realtime 10.0.0-beta.2 → 10.0.0-beta.4
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 +2 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +18 -13
- package/dist/nodefony/config/config.js +7 -5
- package/dist/nodefony/config/configSchema.js +18 -0
- package/dist/nodefony/config/defineModuleConfig.js +43 -17
- package/dist/nodefony/decorators/realtimeDecorators.js +69 -19
- package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
- package/dist/nodefony/src/backplane/RedisBackplane.js +22 -6
- package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
- package/dist/nodefony/src/backplane/bootNotices.js +42 -0
- package/dist/nodefony/src/backplane/envelope.js +1 -2
- package/dist/nodefony/src/backplane/originId.js +1 -2
- package/dist/nodefony/src/backplane/publishQueue.js +1 -5
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +16 -6
- package/dist/nodefony/src/guards.js +40 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +2 -6
- package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
- package/dist/nodefony/src/server/RealtimeController.js +226 -82
- package/dist/nodefony/src/server/RealtimeHub.js +32 -54
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +30 -13
- package/dist/nodefony/src/server/deniedDetail.js +1 -2
- package/dist/nodefony/src/server/subscribedAck.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +5 -4
- package/dist/nodefony/src/server/welcomeEnv.js +1 -2
- package/dist/nodefony/src/service/RealtimeService.js +4 -4
- package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
- package/dist/nodefony/testing/index.js +118 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/nodefony/config/configSchema.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
- package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
- package/dist/types/nodefony/src/guards.d.ts +32 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +82 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
- package/dist/types/nodefony/testing/index.d.ts +8 -3
- package/docs/actions.md +129 -45
- package/docs/architecture.md +143 -135
- package/docs/cadence-et-contre-pression.md +643 -0
- package/docs/configuration.md +126 -97
- package/docs/cookbook-chat.md +50 -43
- package/docs/index.md +63 -38
- package/docs/observabilite.md +58 -56
- package/docs/protocole.md +110 -76
- package/docs/securite.md +128 -117
- package/docs/vocabulaire.md +133 -129
- package/package.json +17 -13
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
- package/docs/couverture-bancs.md +0 -139
package/README.md
CHANGED
|
@@ -31,9 +31,9 @@ Le client navigateur **n'est pas dans ce module** : il vit dans le cœur, import
|
|
|
31
31
|
du serveur (isomorphisme). Le subpath est **`nodefony/client`**.
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
|
-
import {
|
|
34
|
+
import { NodefonySocket } from "nodefony/client";
|
|
35
35
|
|
|
36
|
-
const socket =
|
|
36
|
+
const socket = NodefonySocket.shared({ url: "wss://app.com/realtime" });
|
|
37
37
|
|
|
38
38
|
await socket.subscribe("chat:room");
|
|
39
39
|
socket.on("chat:room", (msg) => console.log(msg));
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// \0@oxc-project+runtime@0.155.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);
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
}
|
|
8
|
-
//#endregion
|
|
9
8
|
export { __decorate as default };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import config from "./nodefony/config/config.js";
|
|
2
|
-
import { defineRealtimeConfig, realtimeConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
2
|
+
import { buildRealtimeConfig, defineRealtimeConfig, realtimeConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
3
|
+
import { isCallable, isRecord } from "./nodefony/src/guards.js";
|
|
4
|
+
import { realtimeBackplaneDownNotice, realtimeLocalOnlyNotice } from "./nodefony/src/backplane/bootNotices.js";
|
|
3
5
|
import resolveBackplaneOriginId from "./nodefony/src/backplane/originId.js";
|
|
4
6
|
import { LoopbackBackplane } from "./nodefony/src/backplane/LoopbackBackplane.js";
|
|
5
7
|
import { ANONYMOUS_REALTIME_TOKEN } from "./nodefony/src/server/AnonymousRealtimeToken.js";
|
|
@@ -10,15 +12,14 @@ import { BROWSER_ORIGIN, MAX_CLIENT_SEVERITY, createSyslogUplinkHandler } from "
|
|
|
10
12
|
import { WsConnectionTransport } from "./nodefony/src/transport/WsConnectionTransport.js";
|
|
11
13
|
import { RealtimeController } from "./nodefony/src/server/RealtimeController.js";
|
|
12
14
|
import { ServerRealtimeSocket, serverSocket } from "./nodefony/src/server/ServerRealtimeSocket.js";
|
|
13
|
-
import { REDIS_RT_CHANNEL, RedisBackplane, createRedisServiceTransport, resolveRedisChannel } from "./nodefony/src/backplane/RedisBackplane.js";
|
|
15
|
+
import { REDIS_RT_CHANNEL, RedisBackplane, createRedisServiceTransport, isRedisPublisher, isRedisSubscriber, resolveRedisChannel } from "./nodefony/src/backplane/RedisBackplane.js";
|
|
14
16
|
import { ClusterBackplane, processIpcTransport } from "./nodefony/src/backplane/ClusterBackplane.js";
|
|
15
17
|
import { getBackplaneDriver, listBackplaneDrivers, registerBackplaneDriver } from "./nodefony/src/backplane/backplaneRegistry.js";
|
|
16
18
|
import { ClusterProbeClient, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, mergeClusterHealth, processProbeTransport, setClusterProbeClient } from "./nodefony/src/cluster/ClusterProbeClient.js";
|
|
17
19
|
import { buildOwnHealth, buildRealtimeHealth, createRealtimeAdminApi } from "./nodefony/src/server/RealtimeAdminApi.js";
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
//#region index.ts
|
|
20
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
21
|
+
import { IDENTITY_CHANGED_EVENT, Module, services, withTimeout } from "nodefony";
|
|
22
|
+
// index.ts
|
|
22
23
|
/**
|
|
23
24
|
* @nodefony/realtime — Couche realtime serveur Nodefony.
|
|
24
25
|
*
|
|
@@ -51,13 +52,13 @@ registerBackplaneDriver(LoopbackBackplane.driver, () => null);
|
|
|
51
52
|
registerBackplaneDriver(ClusterBackplane.driver, (ctx) => ctx.role === "WORKER" && process.env.NF_CLUSTER === "1" ? new ClusterBackplane(processIpcTransport, ctx.originId) : null);
|
|
52
53
|
registerBackplaneDriver(RedisBackplane.driver, (ctx) => {
|
|
53
54
|
const redisService = ctx.module.kernel?.container?.get("redis");
|
|
54
|
-
if (!redisService) {
|
|
55
|
+
if (!isRecord(redisService) || !isCallable(redisService.getClient)) {
|
|
55
56
|
ctx.module.log(`driver "${RedisBackplane.driver}" : module @nodefony/redis absent (non listé dans @modules) — RealtimeHub reste local`, "WARNING");
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
const publisher = redisService.getClient("publish");
|
|
59
60
|
const subscriber = redisService.getClient("subscribe");
|
|
60
|
-
if (!publisher || !subscriber) {
|
|
61
|
+
if (!isRedisPublisher(publisher) || !isRedisSubscriber(subscriber)) {
|
|
61
62
|
ctx.module.log(`driver "${RedisBackplane.driver}" : connexions Redis publish/subscribe indisponibles — RealtimeHub reste local`, "WARNING");
|
|
62
63
|
return null;
|
|
63
64
|
}
|
|
@@ -96,7 +97,7 @@ let Realtime = class Realtime extends Module {
|
|
|
96
97
|
* sans dupliquer la validation.
|
|
97
98
|
*/
|
|
98
99
|
async onKernelRegister() {
|
|
99
|
-
const validated =
|
|
100
|
+
const validated = buildRealtimeConfig(this.options);
|
|
100
101
|
this.options = validated;
|
|
101
102
|
return this;
|
|
102
103
|
}
|
|
@@ -117,7 +118,10 @@ let Realtime = class Realtime extends Module {
|
|
|
117
118
|
return this;
|
|
118
119
|
}
|
|
119
120
|
const broker = this.kernel?.container?.get("adminBroker");
|
|
120
|
-
if (broker &&
|
|
121
|
+
if (isRecord(broker) && isCallable(broker.has) && isCallable(broker.register) && broker.has("realtime") !== true) broker.register(createRealtimeAdminApi());
|
|
122
|
+
this.kernel?.on(IDENTITY_CHANGED_EVENT, (event) => {
|
|
123
|
+
getRealtimeHub().refreshIdentity(event.identifier);
|
|
124
|
+
});
|
|
121
125
|
await this.#wireBackplane();
|
|
122
126
|
this.#wireClusterProbe(this.#clusterRole);
|
|
123
127
|
return this;
|
|
@@ -157,12 +161,14 @@ let Realtime = class Realtime extends Module {
|
|
|
157
161
|
});
|
|
158
162
|
if (!backplane) {
|
|
159
163
|
this.log(`realtime backplane driver=${driverName} kind=local cross-pod=no (hub local)`, "INFO");
|
|
164
|
+
this.kernel?.reportBootNotice(realtimeLocalOnlyNotice(driverName));
|
|
160
165
|
return;
|
|
161
166
|
}
|
|
162
167
|
try {
|
|
163
168
|
await this.#startWithTimeout(backplane, BACKPLANE_START_TIMEOUT_MS);
|
|
164
169
|
} catch (e) {
|
|
165
|
-
this.log(`realtime backplane driver=${driverName} indisponible (${e.message}) — fallback hub LOCAL, boot poursuivi (pas de fan-out cross-pod)`, "WARNING");
|
|
170
|
+
this.log(`realtime backplane driver=${driverName} indisponible (${e instanceof Error ? e.message : String(e)}) — fallback hub LOCAL, boot poursuivi (pas de fan-out cross-pod)`, "WARNING");
|
|
171
|
+
this.kernel?.reportBootNotice(realtimeBackplaneDownNotice(driverName, e instanceof Error ? e.message : String(e)));
|
|
166
172
|
return;
|
|
167
173
|
}
|
|
168
174
|
hub.setBackplane(backplane);
|
|
@@ -212,7 +218,6 @@ let Realtime = class Realtime extends Module {
|
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
};
|
|
215
|
-
Realtime = __decorate([services([RealtimeService])
|
|
221
|
+
Realtime = __decorate([services([RealtimeService])], Realtime);
|
|
216
222
|
var realtime_default = Realtime;
|
|
217
|
-
//#endregion
|
|
218
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 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
// nodefony/config/config.ts
|
|
3
3
|
/**
|
|
4
4
|
* @nodefony/realtime — CONFIGURATION DU MODULE (schéma Zod = source unique).
|
|
5
5
|
*
|
|
@@ -24,9 +24,12 @@ import { z } from "zod";
|
|
|
24
24
|
*/
|
|
25
25
|
const probeSchema = z.strictObject({ enabled: z.boolean().default(true).describe("Branche le ClusterProbeClient (sonde agrégée pod, Phase 4c) en worker `nodefony cluster`. true = sonde active (timer + IPC). false = bypass total (0 timer / 0 listener / 0 IPC, l'endpoint santé sert la vue per-instance). Override env : NF_CLUSTER_PROBE=0 force la désactivation.") }).describe("Sonde agrégée pod (Phase 4c).");
|
|
26
26
|
const backplaneSchema = z.strictObject({
|
|
27
|
-
driver: z.string().default("loopback").describe("Nom du driver IBackplane, résolu dans le registre de drivers (`listBackplaneDrivers()`) — PAS d'enum en dur ici : la liste réelle est la source de vérité du registre, ouverte aux drivers custom utilisateur (`registerBackplaneDriver(name, factory)`). Natifs : `loopback` (mono, hub local sans IPC), `cluster` (IPC workers `nodefony cluster`, actif si NF_CLUSTER=1), `redis` (multi-host pub/sub). Driver inconnu au boot → warn fail-soft, le hub reste local."),
|
|
28
|
-
namespace: z.string().min(1).regex(/^[\w.-]+$/).optional().describe("Cloison logique du transport partagé (drivers cross-pod : redis, futur kafka) — suffixe le canal pub/sub (`nodefony:realtime:<ns>`). Le `database` Redis ne cloisonnant PAS le pub/sub, deux apps sur un Redis mutualisé SANS namespace se parleraient (cross-talk). Défaut : dérivé du nom d'app (`kernel.projectName`). À poser EXPLICITEMENT quand deux déploiements de la même app (staging/prod) partagent un Redis. Caractères : alphanumériques, `_`, `.`, `-`. Env : `NF_REALTIME_BACKPLANE_NAMESPACE` (prioritaire — le namespace distingue des DÉPLOIEMENTS, il n'a pas à être figé dans le code)."),
|
|
29
|
-
secret: z.string().min(32).optional().describe("Secret de SCELLEMENT des messages du backplane, partagé par tous les pods de l'app (drivers à transport PARTAGÉ : redis, futurs drivers cross-host). Un bus pub/sub n'authentifie PAS l'émetteur : sans ce secret, quiconque écrit dans le Redis (autre app d'un Redis mutualisé, credential fuité, SSRF) publie sur les canaux de TOUS les pods. Posé, chaque message porte un HMAC-SHA256 et l'ingress devient fail-closed strict (non scellé ou mal scellé = ignoré, aucun downgrade). Sans lui, le boot ALERTE et le bus reste ouvert. Inutile pour `cluster` (IPC master↔workers : aucun tiers ne peut y écrire). Env : `NF_REALTIME_BACKPLANE_SECRET`. Minimum 32 caractères.").meta({
|
|
27
|
+
driver: z.string().default("loopback").describe("Nom du driver IBackplane, résolu dans le registre de drivers (`listBackplaneDrivers()`) — PAS d'enum en dur ici : la liste réelle est la source de vérité du registre, ouverte aux drivers custom utilisateur (`registerBackplaneDriver(name, factory)`). Natifs : `loopback` (mono, hub local sans IPC), `cluster` (IPC workers `nodefony cluster`, actif si NF_CLUSTER=1), `redis` (multi-host pub/sub). Driver inconnu au boot → warn fail-soft, le hub reste local.").meta({ env: "NF_REALTIME_DRIVER" }),
|
|
28
|
+
namespace: z.string().min(1).regex(/^[\w.-]+$/).optional().describe("Cloison logique du transport partagé (drivers cross-pod : redis, futur kafka) — suffixe le canal pub/sub (`nodefony:realtime:<ns>`). Le `database` Redis ne cloisonnant PAS le pub/sub, deux apps sur un Redis mutualisé SANS namespace se parleraient (cross-talk). Défaut : dérivé du nom d'app (`kernel.projectName`). À poser EXPLICITEMENT quand deux déploiements de la même app (staging/prod) partagent un Redis. Caractères : alphanumériques, `_`, `.`, `-`. Env : `NF_REALTIME_BACKPLANE_NAMESPACE` (prioritaire — le namespace distingue des DÉPLOIEMENTS, il n'a pas à être figé dans le code).").meta({ env: "NF_REALTIME_BACKPLANE_NAMESPACE" }),
|
|
29
|
+
secret: z.string().min(32).optional().describe("Secret de SCELLEMENT des messages du backplane, partagé par tous les pods de l'app (drivers à transport PARTAGÉ : redis, futurs drivers cross-host). Un bus pub/sub n'authentifie PAS l'émetteur : sans ce secret, quiconque écrit dans le Redis (autre app d'un Redis mutualisé, credential fuité, SSRF) publie sur les canaux de TOUS les pods. Posé, chaque message porte un HMAC-SHA256 et l'ingress devient fail-closed strict (non scellé ou mal scellé = ignoré, aucun downgrade). Sans lui, le boot ALERTE et le bus reste ouvert. Inutile pour `cluster` (IPC master↔workers : aucun tiers ne peut y écrire). Env : `NF_REALTIME_BACKPLANE_SECRET`. Minimum 32 caractères.").meta({
|
|
30
|
+
env: "NF_REALTIME_BACKPLANE_SECRET",
|
|
31
|
+
secret: true
|
|
32
|
+
}),
|
|
30
33
|
maxQueueBytes: z.number().int().nonnegative().default(1 << 23).describe("Seuil d'octets PUBLIÉS mais pas encore acquittés par le bus, au-delà duquel les publications suivantes sont JETÉES (drivers à transport réseau : redis, drivers cross-host userland). Un `publish` de backplane est fire-and-forget : quand le bus ne draine plus, la file interne du client réseau grossit sans limite (583 MB observés sous rafale sur le banc multi-pods). Même doctrine que le back-pressure WS : on sacrifie du fan-out cross-pod pour tenir la mémoire du pod, jamais en silence (compteurs `backplane.queue` dans la sonde + Studio, alerte au franchissement). Défaut 8 MiB, soit trois ordres de grandeur au-dessus d'un régime sain (bus acquitté en quelques ms). `0` = illimité (opt-out explicite : la mémoire n'est alors bornée par rien).")
|
|
31
34
|
}).describe("Driver IBackplane (fan-out cluster realtime cross-process).");
|
|
32
35
|
const clusterSchema = z.strictObject({ probe: probeSchema.default(() => probeSchema.parse({})) }).describe("Comportement spécifique au mode cluster (worker IPC).");
|
|
@@ -59,5 +62,4 @@ const realtimeConfigSchema = z.strictObject({
|
|
|
59
62
|
* valides par construction ; passés au `super(..., config)` du Module class.
|
|
60
63
|
*/
|
|
61
64
|
const config = realtimeConfigSchema.parse({});
|
|
62
|
-
//#endregion
|
|
63
65
|
export { config as default, realtimeConfigSchema };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { realtimeConfigSchema } from "./config.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// nodefony/config/configSchema.ts
|
|
4
|
+
/**
|
|
5
|
+
* JSON Schema de la configuration de `@nodefony/realtime` — lu SANS démarrer
|
|
6
|
+
* l'application, par `nodefony env` et la notice `.env.example`, qui y
|
|
7
|
+
* trouvent les noms courts (`.meta({ env })`).
|
|
8
|
+
*
|
|
9
|
+
* Déclaré dans `package.json` → `nodefony.configSchema` ; le nom de l'export
|
|
10
|
+
* est FIXE. Fichier FEUILLE : il n'importe que zod et le schéma, et n'ouvre
|
|
11
|
+
* rien à l'import — une commande qui ne démarre pas l'application l'exécute.
|
|
12
|
+
*
|
|
13
|
+
* @returns le JSON Schema, métadonnées Nodefony comprises.
|
|
14
|
+
*/
|
|
15
|
+
function configJsonSchema() {
|
|
16
|
+
return z.toJSONSchema(realtimeConfigSchema);
|
|
17
|
+
}
|
|
18
|
+
export { configJsonSchema };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { realtimeConfigSchema } from "./config.js";
|
|
2
|
+
import { configJsonSchema } from "./configSchema.js";
|
|
2
3
|
import { parseModuleConfig } from "nodefony";
|
|
3
|
-
|
|
4
|
-
//#region nodefony/config/defineModuleConfig.ts
|
|
4
|
+
// nodefony/config/defineModuleConfig.ts
|
|
5
5
|
/**
|
|
6
6
|
* Builder type-safe de la configuration de `@nodefony/realtime`.
|
|
7
7
|
*
|
|
@@ -20,11 +20,13 @@ import { z } from "zod";
|
|
|
20
20
|
* faire dans un JSON Schema Studio). L'instance, si fournie, prend précédence
|
|
21
21
|
* sur `driver` côté service ; ce dernier reste source d'introspection/UI.
|
|
22
22
|
*
|
|
23
|
-
* **
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* (
|
|
27
|
-
*
|
|
23
|
+
* **Variables d'environnement** : `NF_REALTIME_DRIVER`,
|
|
24
|
+
* `NF_REALTIME_BACKPLANE_SECRET` et `NF_REALTIME_BACKPLANE_NAMESPACE` ne se
|
|
25
|
+
* lisent PAS ici. Chacune est déclarée sur son champ dans `config.ts`
|
|
26
|
+
* (`.meta({ env })`) et posée par le Kernel AVANT la validation : le schéma
|
|
27
|
+
* les valide comme une valeur de l'application (un secret de moins de 32
|
|
28
|
+
* caractères arrête le démarrage). Un appel direct de ce builder, hors Kernel,
|
|
29
|
+
* ne les voit donc pas.
|
|
28
30
|
*
|
|
29
31
|
* @param config - configuration brute (sections omises = défauts sûrs).
|
|
30
32
|
* @param options.backplane - instance `IBackplane` custom (override le driver à
|
|
@@ -33,28 +35,52 @@ import { z } from "zod";
|
|
|
33
35
|
* @throws ZodError si invalide.
|
|
34
36
|
*/
|
|
35
37
|
function defineRealtimeConfig(config = {}, options = {}) {
|
|
38
|
+
return buildRealtimeConfig(config, options);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Le builder pour une entrée NON typée — la config reçue par le module au
|
|
42
|
+
* démarrage, dont le schéma décide seul de la forme. Interne au paquet : une
|
|
43
|
+
* application passe par {@link defineRealtimeConfig}, qui type ce qu'elle écrit.
|
|
44
|
+
*
|
|
45
|
+
* @param config - configuration brute, validée par le schéma
|
|
46
|
+
* @param options.backplane - instance `IBackplane` custom
|
|
47
|
+
* @returns config gelée prête pour `RealtimeService`.
|
|
48
|
+
* @throws BootConfigurationError si invalide.
|
|
49
|
+
*/
|
|
50
|
+
function buildRealtimeConfig(config = {}, options = {}) {
|
|
36
51
|
const parsed = parseModuleConfig(realtimeConfigSchema, config, "@nodefony/realtime");
|
|
37
|
-
const driver = process.env.NF_REALTIME_DRIVER || parsed.backplane.driver;
|
|
38
|
-
const secret = process.env.NF_REALTIME_BACKPLANE_SECRET || parsed.backplane.secret;
|
|
39
|
-
const namespace = process.env.NF_REALTIME_BACKPLANE_NAMESPACE || parsed.backplane.namespace;
|
|
40
52
|
const out = {
|
|
41
53
|
...parsed,
|
|
42
54
|
backplane: {
|
|
43
55
|
...parsed.backplane,
|
|
44
|
-
driver,
|
|
45
|
-
secret,
|
|
46
|
-
namespace,
|
|
47
56
|
instance: options.backplane
|
|
48
57
|
}
|
|
49
58
|
};
|
|
50
|
-
|
|
59
|
+
const frozen = Object.freeze(out);
|
|
60
|
+
built.add(frozen);
|
|
61
|
+
return frozen;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Configs produites par ce builder. Marquer la sortie plutôt que d'en vérifier
|
|
65
|
+
* la forme : une seule appartenance (`has`) prouve que l'objet a traversé le
|
|
66
|
+
* schéma, ce qu'aucune inspection de champs ne dit.
|
|
67
|
+
*/
|
|
68
|
+
const built = /* @__PURE__ */ new WeakSet();
|
|
69
|
+
/**
|
|
70
|
+
* Vrai si la valeur est une config produite par {@link buildRealtimeConfig}
|
|
71
|
+
* (donc validée par le schéma). Lu une fois, à l'init du service.
|
|
72
|
+
*
|
|
73
|
+
* @param value - la config lue sur le module
|
|
74
|
+
* @returns `true` si elle sort du builder
|
|
75
|
+
*/
|
|
76
|
+
function isRealtimeConfig(value) {
|
|
77
|
+
return typeof value === "object" && value !== null && built.has(value);
|
|
51
78
|
}
|
|
52
79
|
/**
|
|
53
80
|
* JSON Schema introspectable de la config realtime — destiné au formulaire
|
|
54
81
|
* d'édition Studio (futur). N'inclut PAS `backplane.instance` (non sérialisable).
|
|
55
82
|
*/
|
|
56
83
|
function realtimeConfigJsonSchema() {
|
|
57
|
-
return
|
|
84
|
+
return configJsonSchema();
|
|
58
85
|
}
|
|
59
|
-
|
|
60
|
-
export { defineRealtimeConfig, realtimeConfigJsonSchema };
|
|
86
|
+
export { buildRealtimeConfig, defineRealtimeConfig, isRealtimeConfig, realtimeConfigJsonSchema };
|
|
@@ -1,14 +1,46 @@
|
|
|
1
|
+
import { isCallable, isRecord } from "../src/guards.js";
|
|
1
2
|
import "reflect-metadata";
|
|
2
|
-
|
|
3
|
+
// nodefony/decorators/realtimeDecorators.ts
|
|
3
4
|
/** Clés de métadonnées posées sur le constructor de la classe controller. */
|
|
4
5
|
const ACTIONS_KEY = "realtime:actions";
|
|
5
6
|
const CHANNELS_KEY = "realtime:channels";
|
|
6
7
|
const INBOUND_KEY = "realtime:inbound";
|
|
7
8
|
const POLICIES_KEY = "realtime:policies";
|
|
9
|
+
/**
|
|
10
|
+
* Lit une métadonnée de classe posée par ces décorateurs : un objet nom → valeur,
|
|
11
|
+
* ou `undefined` quand la classe n'en porte pas. Les valeurs restent `unknown` :
|
|
12
|
+
* `Reflect.defineMetadata` est public, n'importe qui peut écrire sous ces clés.
|
|
13
|
+
* Lu au démarrage et à la poignée de main, jamais par frame.
|
|
14
|
+
*/
|
|
15
|
+
function readMetadataMap(key, ctor) {
|
|
16
|
+
const raw = Reflect.getMetadata(key, ctor);
|
|
17
|
+
return isRecord(raw) ? raw : void 0;
|
|
18
|
+
}
|
|
19
|
+
/** Vrai si la valeur est un tableau de chaînes. */
|
|
20
|
+
function isStringArray(value) {
|
|
21
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
22
|
+
}
|
|
23
|
+
/** Vrai si la valeur a la forme d'une {@link IChannelPolicy}. */
|
|
24
|
+
function isChannelPolicy(value) {
|
|
25
|
+
if (!isRecord(value)) return false;
|
|
26
|
+
const { authenticated, roles, scopes } = value;
|
|
27
|
+
return (authenticated === void 0 || typeof authenticated === "boolean") && (roles === void 0 || isStringArray(roles)) && (scopes === void 0 || isStringArray(scopes));
|
|
28
|
+
}
|
|
29
|
+
/** Vrai si chaque entrée du registre est une {@link IChannelPolicy}. */
|
|
30
|
+
function isPolicyMap(map) {
|
|
31
|
+
for (const name in map) if (!isChannelPolicy(map[name])) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/** Méthode de l'instance que désigne une entrée de registre, ou `undefined`. */
|
|
35
|
+
function methodOf(instance, prop) {
|
|
36
|
+
return typeof prop === "string" || typeof prop === "symbol" ? Reflect.get(instance, prop) : void 0;
|
|
37
|
+
}
|
|
38
|
+
/** Libération d'un canal dont la fabrique décorée n'en a rendu aucune. */
|
|
39
|
+
const NO_DISPOSE = () => {};
|
|
8
40
|
/** Enregistre (idempotent) une politique non vide sous son nom de canal/méthode. */
|
|
9
41
|
function definePolicy(ctor, name, policy) {
|
|
10
42
|
if (!policy || policy.authenticated === void 0 && policy.roles === void 0 && policy.scopes === void 0) return;
|
|
11
|
-
const map =
|
|
43
|
+
const map = { ...readMetadataMap(POLICIES_KEY, ctor) };
|
|
12
44
|
map[name] = policy;
|
|
13
45
|
Reflect.defineMetadata(POLICIES_KEY, map, ctor);
|
|
14
46
|
}
|
|
@@ -21,7 +53,7 @@ function definePolicy(ctor, name, policy) {
|
|
|
21
53
|
const DEFAULT_ACTION_POLICY = Object.freeze({ authenticated: true });
|
|
22
54
|
/** Une politique est-elle DÉJÀ déclarée sous ce nom ? */
|
|
23
55
|
function hasPolicy(ctor, name) {
|
|
24
|
-
const map =
|
|
56
|
+
const map = readMetadataMap(POLICIES_KEY, ctor);
|
|
25
57
|
return map ? Object.prototype.hasOwnProperty.call(map, name) : false;
|
|
26
58
|
}
|
|
27
59
|
/**
|
|
@@ -72,7 +104,7 @@ function hasPolicy(ctor, name) {
|
|
|
72
104
|
function RealtimeAction(method, policy) {
|
|
73
105
|
return function(target, propertyKey) {
|
|
74
106
|
const ctor = target.constructor;
|
|
75
|
-
const map =
|
|
107
|
+
const map = { ...readMetadataMap(ACTIONS_KEY, ctor) };
|
|
76
108
|
map[method] = propertyKey;
|
|
77
109
|
Reflect.defineMetadata(ACTIONS_KEY, map, ctor);
|
|
78
110
|
if (policy) definePolicy(ctor, method, policy);
|
|
@@ -109,7 +141,7 @@ function RealtimeAction(method, policy) {
|
|
|
109
141
|
function RealtimeChannel(channel, policy) {
|
|
110
142
|
return function(target, propertyKey) {
|
|
111
143
|
const ctor = target.constructor;
|
|
112
|
-
const map =
|
|
144
|
+
const map = { ...readMetadataMap(CHANNELS_KEY, ctor) };
|
|
113
145
|
map[channel] = propertyKey;
|
|
114
146
|
Reflect.defineMetadata(CHANNELS_KEY, map, ctor);
|
|
115
147
|
definePolicy(ctor, channel, policy);
|
|
@@ -140,42 +172,58 @@ function RealtimeChannel(channel, policy) {
|
|
|
140
172
|
function RealtimeInbound(method, policy) {
|
|
141
173
|
return function(target, propertyKey) {
|
|
142
174
|
const ctor = target.constructor;
|
|
143
|
-
const map =
|
|
175
|
+
const map = { ...readMetadataMap(INBOUND_KEY, ctor) };
|
|
144
176
|
map[method] = propertyKey;
|
|
145
177
|
Reflect.defineMetadata(INBOUND_KEY, map, ctor);
|
|
146
178
|
definePolicy(ctor, method, policy);
|
|
147
179
|
};
|
|
148
180
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Lecture (lazy, au handshake) du registre des actions décorées.
|
|
183
|
+
*
|
|
184
|
+
* @param instance - le contrôleur de la connexion
|
|
185
|
+
* @param wrap - enveloppe optionnelle de chaque action (résolution des
|
|
186
|
+
* paramètres décorés) ; sans elle, l'action reçoit la charge JSON-RPC brute
|
|
187
|
+
* @returns les handlers par nom d'action, ou `null` si aucune n'est décorée
|
|
188
|
+
*/
|
|
189
|
+
function getRealtimeActions(instance, wrap) {
|
|
190
|
+
const map = readMetadataMap(ACTIONS_KEY, instance.constructor);
|
|
152
191
|
if (!map) return null;
|
|
153
192
|
const out = {};
|
|
154
193
|
for (const [name, prop] of Object.entries(map)) {
|
|
155
|
-
|
|
156
|
-
|
|
194
|
+
if (typeof prop !== "string" && typeof prop !== "symbol") continue;
|
|
195
|
+
const fn = methodOf(instance, prop);
|
|
196
|
+
if (isCallable(fn)) {
|
|
197
|
+
const bound = fn.bind(instance);
|
|
198
|
+
out[name] = wrap !== void 0 ? wrap(bound, prop) : bound;
|
|
199
|
+
}
|
|
157
200
|
}
|
|
158
201
|
return out;
|
|
159
202
|
}
|
|
160
203
|
/** Lecture (lazy, au handshake) du registre des canaux décorés. */
|
|
161
204
|
function getRealtimeChannels(instance) {
|
|
162
|
-
const map =
|
|
205
|
+
const map = readMetadataMap(CHANNELS_KEY, instance.constructor);
|
|
163
206
|
if (!map) return null;
|
|
164
207
|
const out = {};
|
|
165
208
|
for (const [name, prop] of Object.entries(map)) {
|
|
166
|
-
const fn = instance
|
|
167
|
-
if (
|
|
209
|
+
const fn = methodOf(instance, prop);
|
|
210
|
+
if (!isCallable(fn)) continue;
|
|
211
|
+
const factory = fn.bind(instance);
|
|
212
|
+
out[name] = (channel, publish) => {
|
|
213
|
+
const dispose = factory(channel, publish);
|
|
214
|
+
return isCallable(dispose) ? () => void dispose() : NO_DISPOSE;
|
|
215
|
+
};
|
|
168
216
|
}
|
|
169
217
|
return out;
|
|
170
218
|
}
|
|
171
219
|
/** Lecture (lazy, au handshake) du registre des canaux inbound décorés. */
|
|
172
220
|
function getRealtimeInbound(instance) {
|
|
173
|
-
const map =
|
|
221
|
+
const map = readMetadataMap(INBOUND_KEY, instance.constructor);
|
|
174
222
|
if (!map) return null;
|
|
175
223
|
const out = {};
|
|
176
224
|
for (const [name, prop] of Object.entries(map)) {
|
|
177
|
-
const fn = instance
|
|
178
|
-
if (
|
|
225
|
+
const fn = methodOf(instance, prop);
|
|
226
|
+
if (isCallable(fn)) out[name] = fn.bind(instance);
|
|
179
227
|
}
|
|
180
228
|
return out;
|
|
181
229
|
}
|
|
@@ -187,7 +235,10 @@ function getRealtimeInbound(instance) {
|
|
|
187
235
|
* hub realtime, qui l'expose à `@nodefony/security`.
|
|
188
236
|
*/
|
|
189
237
|
function getRealtimeChannelPolicies(instance) {
|
|
190
|
-
|
|
238
|
+
const map = readMetadataMap(POLICIES_KEY, instance.constructor);
|
|
239
|
+
if (map === void 0) return null;
|
|
240
|
+
if (!isPolicyMap(map)) throw new TypeError(`${instance.constructor.name} : politique de canal temps réel illisible`);
|
|
241
|
+
return map;
|
|
191
242
|
}
|
|
192
243
|
/**
|
|
193
244
|
* Préfixes de canaux **broadcast** déclarés statiquement, tous controllers
|
|
@@ -236,5 +287,4 @@ function getDeclaredBroadcastPrefixes() {
|
|
|
236
287
|
function applyDeclaredBroadcastPrefixes(hub) {
|
|
237
288
|
for (const prefix of declaredBroadcastPrefixes) hub.markBroadcastChannel(prefix);
|
|
238
289
|
}
|
|
239
|
-
//#endregion
|
|
240
290
|
export { DEFAULT_ACTION_POLICY, RealtimeAction, RealtimeBroadcast, RealtimeChannel, RealtimeInbound, applyDeclaredBroadcastPrefixes, getDeclaredBroadcastPrefixes, getRealtimeActions, getRealtimeChannelPolicies, getRealtimeChannels, getRealtimeInbound };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import resolveBackplaneOriginId from "./originId.js";
|
|
2
2
|
import { CLUSTER_RT_KIND } from "nodefony";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/backplane/ClusterBackplane.ts
|
|
4
4
|
/**
|
|
5
5
|
* Transport de production : `process.send` (émission) + `process.on("message")`
|
|
6
6
|
* (réception). Standalone — utilisable tel quel. En vrai câblage cluster (Phase 3c),
|
|
@@ -101,5 +101,4 @@ var ClusterBackplane = class ClusterBackplane {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
|
-
//#endregion
|
|
105
104
|
export { ClusterBackplane, ClusterBackplane as default, processIpcTransport };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import resolveBackplaneOriginId from "./originId.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/backplane/LoopbackBackplane.ts
|
|
3
3
|
/**
|
|
4
4
|
* Backplane **mono-process no-op** — implémentation de RÉFÉRENCE du port
|
|
5
5
|
* {@link IBackplane} quand il n'existe **aucun pair** (1 process = 1 pod).
|
|
@@ -36,5 +36,4 @@ var LoopbackBackplane = class LoopbackBackplane {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
//#endregion
|
|
40
39
|
export { LoopbackBackplane, LoopbackBackplane as default };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { isCallable, isRecord, isThenable } from "../guards.js";
|
|
1
2
|
import resolveBackplaneOriginId from "./originId.js";
|
|
2
3
|
import { openBackplaneEnvelope, sealBackplaneEnvelope } from "./envelope.js";
|
|
3
4
|
import { BackplanePublishQueue } from "./publishQueue.js";
|
|
4
|
-
|
|
5
|
+
// nodefony/src/backplane/RedisBackplane.ts
|
|
5
6
|
/**
|
|
6
7
|
* BASE du canal Redis **dédié** transportant les enveloppes realtime entre pods.
|
|
7
8
|
* Un seul canal pub/sub par app (les canaux logiques voyagent DANS l'enveloppe,
|
|
@@ -43,20 +44,36 @@ function resolveRedisChannel(namespace) {
|
|
|
43
44
|
* Le listener Redis est conservé pour un `unsubscribe(channel, listener)` ciblé
|
|
44
45
|
* (n'arrache pas d'éventuels autres abonnés du même client).
|
|
45
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* Vrai si la valeur expose la surface d'un client Redis publisher — vérifié au
|
|
49
|
+
* démarrage, sur le client que rend le service `redis`.
|
|
50
|
+
*/
|
|
51
|
+
function isRedisPublisher(value) {
|
|
52
|
+
return isRecord(value) && isCallable(value.publish);
|
|
53
|
+
}
|
|
54
|
+
/** Vrai si la valeur expose la surface d'un client Redis subscriber dédié. */
|
|
55
|
+
function isRedisSubscriber(value) {
|
|
56
|
+
return isRecord(value) && isCallable(value.subscribe) && isCallable(value.unsubscribe);
|
|
57
|
+
}
|
|
58
|
+
/** Ramène l'acquittement d'une commande Redis au contrat du transport. */
|
|
59
|
+
function settled(ack) {
|
|
60
|
+
return isThenable(ack) ? ack.then(() => void 0) : void 0;
|
|
61
|
+
}
|
|
46
62
|
function createRedisServiceTransport(publisher, subscriber) {
|
|
47
63
|
let listener = null;
|
|
48
64
|
return {
|
|
49
65
|
publish(channel, message) {
|
|
50
|
-
|
|
66
|
+
const ack = publisher.publish(channel, message);
|
|
67
|
+
return isThenable(ack) ? ack : void 0;
|
|
51
68
|
},
|
|
52
69
|
subscribe(channel, onMessage) {
|
|
53
70
|
listener = (message) => onMessage(message);
|
|
54
|
-
return subscriber.subscribe(channel, listener);
|
|
71
|
+
return settled(subscriber.subscribe(channel, listener));
|
|
55
72
|
},
|
|
56
73
|
unsubscribe(channel) {
|
|
57
74
|
const current = listener;
|
|
58
75
|
listener = null;
|
|
59
|
-
return current ? subscriber.unsubscribe(channel, current) : subscriber.unsubscribe(channel);
|
|
76
|
+
return settled(current ? subscriber.unsubscribe(channel, current) : subscriber.unsubscribe(channel));
|
|
60
77
|
}
|
|
61
78
|
};
|
|
62
79
|
}
|
|
@@ -172,5 +189,4 @@ var RedisBackplane = class RedisBackplane {
|
|
|
172
189
|
};
|
|
173
190
|
}
|
|
174
191
|
};
|
|
175
|
-
|
|
176
|
-
export { REDIS_RT_CHANNEL, RedisBackplane, RedisBackplane as default, createRedisServiceTransport, resolveRedisChannel };
|
|
192
|
+
export { REDIS_RT_CHANNEL, RedisBackplane, RedisBackplane as default, createRedisServiceTransport, isRedisPublisher, isRedisSubscriber, resolveRedisChannel };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/backplane/backplaneRegistry.ts
|
|
2
2
|
const drivers = /* @__PURE__ */ new Map();
|
|
3
3
|
/**
|
|
4
4
|
* Enregistre (ou remplace) la fabrique d'un driver backplane. Appelé par les
|
|
@@ -16,5 +16,4 @@ function getBackplaneDriver(name) {
|
|
|
16
16
|
function listBackplaneDrivers() {
|
|
17
17
|
return [...drivers.keys()];
|
|
18
18
|
}
|
|
19
|
-
//#endregion
|
|
20
19
|
export { getBackplaneDriver, listBackplaneDrivers, registerBackplaneDriver };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// nodefony/src/backplane/bootNotices.ts
|
|
2
|
+
/** Le geste qui rend le temps réel partagé entre processus et pods. */
|
|
3
|
+
const REDIS_BACKPLANE_FIX = "use(\"@nodefony/realtime\", { backplane: { driver: \"redis\" } }) + NF_REDIS_URL=redis://…";
|
|
4
|
+
/**
|
|
5
|
+
* Le temps réel ne dépasse pas ce processus : aucun backplane actif (driver
|
|
6
|
+
* `loopback`, `cluster` hors d'un worker, ou fabrique inactive ici).
|
|
7
|
+
*
|
|
8
|
+
* Simple information en développement mono-processus — c'est le cas normal —
|
|
9
|
+
* mais à savoir avant de déployer plusieurs exemplaires : un message publié
|
|
10
|
+
* sur l'un n'atteindrait pas les clients connectés aux autres.
|
|
11
|
+
*
|
|
12
|
+
* @param driver - le driver déclaré.
|
|
13
|
+
* @returns le point à déclarer.
|
|
14
|
+
*/
|
|
15
|
+
function realtimeLocalOnlyNotice(driver) {
|
|
16
|
+
return {
|
|
17
|
+
code: "REALTIME_LOCAL_ONLY",
|
|
18
|
+
level: "info",
|
|
19
|
+
message: `Temps réel limité à ce processus (backplane « ${driver} » inactif ici)`,
|
|
20
|
+
fix: REDIS_BACKPLANE_FIX
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Le backplane déclaré n'a pas pu démarrer — le hub est retombé en local.
|
|
25
|
+
*
|
|
26
|
+
* Distinct de {@link realtimeLocalOnlyNotice} : ici, l'application a DEMANDÉ
|
|
27
|
+
* un fan-out entre exemplaires et ne l'a pas. Deux pods servent alors deux
|
|
28
|
+
* mondes temps réel disjoints, sans erreur visible.
|
|
29
|
+
*
|
|
30
|
+
* @param driver - le driver déclaré.
|
|
31
|
+
* @param reason - la cause (message de l'échec).
|
|
32
|
+
* @returns le point à déclarer.
|
|
33
|
+
*/
|
|
34
|
+
function realtimeBackplaneDownNotice(driver, reason) {
|
|
35
|
+
return {
|
|
36
|
+
code: "REALTIME_BACKPLANE_DOWN",
|
|
37
|
+
level: "warning",
|
|
38
|
+
message: `Backplane temps réel « ${driver} » indisponible (${reason}) — retombé sur ce seul processus`,
|
|
39
|
+
fix: driver === "redis" ? "vérifier NF_REDIS_URL et que Redis répond" : void 0
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export { realtimeBackplaneDownNotice, realtimeLocalOnlyNotice };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/backplane/envelope.ts
|
|
3
3
|
/**
|
|
4
4
|
* **Sceau d'enveloppe backplane** — authenticité des messages transportés sur un
|
|
5
5
|
* bus **PARTAGÉ** (Redis pub/sub, et tout driver userland cross-host : NATS,
|
|
@@ -106,5 +106,4 @@ function openBackplaneEnvelope(raw, secret) {
|
|
|
106
106
|
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
|
|
107
107
|
return msg;
|
|
108
108
|
}
|
|
109
|
-
//#endregion
|
|
110
109
|
export { openBackplaneEnvelope, sealBackplaneEnvelope };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hostname } from "node:os";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/backplane/originId.ts
|
|
4
4
|
/**
|
|
5
5
|
* Résout l'identifiant d'origine d'un backplane — l'étiquette que l'anti-écho
|
|
6
6
|
* compare pour jeter les messages qui reviennent à leur émetteur.
|
|
@@ -31,5 +31,4 @@ function resolveBackplaneOriginId() {
|
|
|
31
31
|
if (!host) return randomUUID();
|
|
32
32
|
return `${host}:${process.pid}`;
|
|
33
33
|
}
|
|
34
|
-
//#endregion
|
|
35
34
|
export { resolveBackplaneOriginId as default, resolveBackplaneOriginId };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isThenable } from "../guards.js";
|
|
1
2
|
/**
|
|
2
3
|
* Seuil par défaut d'octets en vol vers le bus : **8 MiB**, aligné sur le seuil de
|
|
3
4
|
* coupure d'un slow-consumer WS. Un bus sain acquitte en quelques millisecondes
|
|
@@ -5,10 +6,6 @@
|
|
|
5
6
|
* au-dessus du régime nominal — le pod a un problème bien avant d'y arriver.
|
|
6
7
|
*/
|
|
7
8
|
const DEFAULT_MAX_QUEUE_BYTES = 1 << 23;
|
|
8
|
-
/** Vrai si la valeur se comporte comme une promesse (transport asynchrone). */
|
|
9
|
-
function isThenable(v) {
|
|
10
|
-
return typeof v === "object" && v !== null && typeof v.then === "function";
|
|
11
|
-
}
|
|
12
9
|
/**
|
|
13
10
|
* File d'envoi bornée : décide d'admettre ou de jeter une publication, puis suit
|
|
14
11
|
* son acquittement pour rendre la place.
|
|
@@ -92,5 +89,4 @@ var BackplanePublishQueue = class {
|
|
|
92
89
|
};
|
|
93
90
|
}
|
|
94
91
|
};
|
|
95
|
-
//#endregion
|
|
96
92
|
export { BackplanePublishQueue, BackplanePublishQueue as default, DEFAULT_MAX_QUEUE_BYTES };
|