@nodefony/realtime 10.0.0-beta.2 → 10.0.0-beta.3
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.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +17 -10
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/config/configSchema.js +19 -0
- package/dist/nodefony/config/defineModuleConfig.js +42 -15
- package/dist/nodefony/decorators/realtimeDecorators.js +68 -17
- package/dist/nodefony/src/backplane/RedisBackplane.js +21 -4
- package/dist/nodefony/src/backplane/bootNotices.js +43 -0
- package/dist/nodefony/src/backplane/publishQueue.js +1 -4
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +15 -4
- package/dist/nodefony/src/guards.js +41 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -4
- package/dist/nodefony/src/server/RealtimeController.js +120 -50
- package/dist/nodefony/src/server/RealtimeHub.js +29 -51
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +13 -5
- package/dist/nodefony/src/server/syslogUplink.js +4 -2
- package/dist/nodefony/src/service/RealtimeService.js +3 -2
- package/dist/nodefony/testing/index.js +41 -1
- 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/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- 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 +24 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +111 -28
- package/docs/architecture.md +77 -75
- package/docs/cadence-et-contre-pression.md +634 -0
- package/docs/configuration.md +39 -34
- package/docs/cookbook-chat.md +31 -27
- package/docs/index.md +35 -21
- package/docs/observabilite.md +17 -15
- package/docs/protocole.md +74 -63
- package/docs/securite.md +49 -49
- package/docs/vocabulaire.md +76 -75
- package/package.json +16 -12
- 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,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.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);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.153.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
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,14 +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 __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
19
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
20
|
-
import { Kernel, Module, services, withTimeout } from "nodefony";
|
|
20
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js";
|
|
21
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
22
|
+
import { IDENTITY_CHANGED_EVENT, Kernel, Module, services, withTimeout } from "nodefony";
|
|
21
23
|
//#region index.ts
|
|
22
24
|
/**
|
|
23
25
|
* @nodefony/realtime — Couche realtime serveur Nodefony.
|
|
@@ -51,13 +53,13 @@ registerBackplaneDriver(LoopbackBackplane.driver, () => null);
|
|
|
51
53
|
registerBackplaneDriver(ClusterBackplane.driver, (ctx) => ctx.role === "WORKER" && process.env.NF_CLUSTER === "1" ? new ClusterBackplane(processIpcTransport, ctx.originId) : null);
|
|
52
54
|
registerBackplaneDriver(RedisBackplane.driver, (ctx) => {
|
|
53
55
|
const redisService = ctx.module.kernel?.container?.get("redis");
|
|
54
|
-
if (!redisService) {
|
|
56
|
+
if (!isRecord(redisService) || !isCallable(redisService.getClient)) {
|
|
55
57
|
ctx.module.log(`driver "${RedisBackplane.driver}" : module @nodefony/redis absent (non listé dans @modules) — RealtimeHub reste local`, "WARNING");
|
|
56
58
|
return null;
|
|
57
59
|
}
|
|
58
60
|
const publisher = redisService.getClient("publish");
|
|
59
61
|
const subscriber = redisService.getClient("subscribe");
|
|
60
|
-
if (!publisher || !subscriber) {
|
|
62
|
+
if (!isRedisPublisher(publisher) || !isRedisSubscriber(subscriber)) {
|
|
61
63
|
ctx.module.log(`driver "${RedisBackplane.driver}" : connexions Redis publish/subscribe indisponibles — RealtimeHub reste local`, "WARNING");
|
|
62
64
|
return null;
|
|
63
65
|
}
|
|
@@ -96,7 +98,7 @@ let Realtime = class Realtime extends Module {
|
|
|
96
98
|
* sans dupliquer la validation.
|
|
97
99
|
*/
|
|
98
100
|
async onKernelRegister() {
|
|
99
|
-
const validated =
|
|
101
|
+
const validated = buildRealtimeConfig(this.options);
|
|
100
102
|
this.options = validated;
|
|
101
103
|
return this;
|
|
102
104
|
}
|
|
@@ -117,7 +119,10 @@ let Realtime = class Realtime extends Module {
|
|
|
117
119
|
return this;
|
|
118
120
|
}
|
|
119
121
|
const broker = this.kernel?.container?.get("adminBroker");
|
|
120
|
-
if (broker &&
|
|
122
|
+
if (isRecord(broker) && isCallable(broker.has) && isCallable(broker.register) && broker.has("realtime") !== true) broker.register(createRealtimeAdminApi());
|
|
123
|
+
this.kernel?.on(IDENTITY_CHANGED_EVENT, (event) => {
|
|
124
|
+
getRealtimeHub().refreshIdentity(event.identifier);
|
|
125
|
+
});
|
|
121
126
|
await this.#wireBackplane();
|
|
122
127
|
this.#wireClusterProbe(this.#clusterRole);
|
|
123
128
|
return this;
|
|
@@ -157,12 +162,14 @@ let Realtime = class Realtime extends Module {
|
|
|
157
162
|
});
|
|
158
163
|
if (!backplane) {
|
|
159
164
|
this.log(`realtime backplane driver=${driverName} kind=local cross-pod=no (hub local)`, "INFO");
|
|
165
|
+
this.kernel?.reportBootNotice(realtimeLocalOnlyNotice(driverName));
|
|
160
166
|
return;
|
|
161
167
|
}
|
|
162
168
|
try {
|
|
163
169
|
await this.#startWithTimeout(backplane, BACKPLANE_START_TIMEOUT_MS);
|
|
164
170
|
} catch (e) {
|
|
165
|
-
this.log(`realtime backplane driver=${driverName} indisponible (${e.message}) — fallback hub LOCAL, boot poursuivi (pas de fan-out cross-pod)`, "WARNING");
|
|
171
|
+
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");
|
|
172
|
+
this.kernel?.reportBootNotice(realtimeBackplaneDownNotice(driverName, e instanceof Error ? e.message : String(e)));
|
|
166
173
|
return;
|
|
167
174
|
}
|
|
168
175
|
hub.setBackplane(backplane);
|
|
@@ -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).");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { realtimeConfigSchema } from "./config.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region 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
|
+
//#endregion
|
|
19
|
+
export { configJsonSchema };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realtimeConfigSchema } from "./config.js";
|
|
2
|
+
import { configJsonSchema } from "./configSchema.js";
|
|
2
3
|
import { parseModuleConfig } from "nodefony";
|
|
3
|
-
import { z } from "zod";
|
|
4
4
|
//#region nodefony/config/defineModuleConfig.ts
|
|
5
5
|
/**
|
|
6
6
|
* Builder type-safe de la configuration de `@nodefony/realtime`.
|
|
@@ -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,53 @@ 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
86
|
//#endregion
|
|
60
|
-
export { defineRealtimeConfig, realtimeConfigJsonSchema };
|
|
87
|
+
export { buildRealtimeConfig, defineRealtimeConfig, isRealtimeConfig, realtimeConfigJsonSchema };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCallable, isRecord } from "../src/guards.js";
|
|
1
2
|
import "reflect-metadata";
|
|
2
3
|
//#region nodefony/decorators/realtimeDecorators.ts
|
|
3
4
|
/** Clés de métadonnées posées sur le constructor de la classe controller. */
|
|
@@ -5,10 +6,41 @@ 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
|
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
@@ -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
|
}
|
|
@@ -173,4 +190,4 @@ var RedisBackplane = class RedisBackplane {
|
|
|
173
190
|
}
|
|
174
191
|
};
|
|
175
192
|
//#endregion
|
|
176
|
-
export { REDIS_RT_CHANNEL, RedisBackplane, RedisBackplane as default, createRedisServiceTransport, resolveRedisChannel };
|
|
193
|
+
export { REDIS_RT_CHANNEL, RedisBackplane, RedisBackplane as default, createRedisServiceTransport, isRedisPublisher, isRedisSubscriber, resolveRedisChannel };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region 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
|
+
//#endregion
|
|
43
|
+
export { realtimeBackplaneDownNotice, realtimeLocalOnlyNotice };
|
|
@@ -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.
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
1
2
|
import { CLUSTER_PROBE_CTL_KIND, CLUSTER_PROBE_KIND, CLUSTER_PROBE_SNAPSHOT_KIND, RichProcessProbe, isClusterMessage, isClusterProbeEnrich, readOrmRich } from "nodefony";
|
|
2
3
|
//#region nodefony/src/cluster/ClusterProbeClient.ts
|
|
4
|
+
/**
|
|
5
|
+
* Vrai si la valeur est une liste de santés d'instance. Le snapshot vient du
|
|
6
|
+
* master du cluster par IPC — un process de la MÊME application, pas le
|
|
7
|
+
* réseau : on vérifie la forme qui sert de clé (`instanceId`), le reste est
|
|
8
|
+
* la sonde du même code.
|
|
9
|
+
*/
|
|
10
|
+
function isRealtimeHealthList(value) {
|
|
11
|
+
return Array.isArray(value) && value.every((h) => isRecord(h) && typeof h.instanceId === "string");
|
|
12
|
+
}
|
|
3
13
|
/** Transport de production : `process.send` + `process.on("message")`. */
|
|
4
14
|
const processProbeTransport = {
|
|
5
15
|
send(report) {
|
|
@@ -213,10 +223,11 @@ var ClusterProbeClient = class {
|
|
|
213
223
|
return;
|
|
214
224
|
}
|
|
215
225
|
if (!isClusterMessage(msg) || msg.kind !== CLUSTER_PROBE_SNAPSHOT_KIND) return;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.#
|
|
226
|
+
if (!isRecord(msg)) return;
|
|
227
|
+
const { ts, instances } = msg;
|
|
228
|
+
if (isRealtimeHealthList(instances)) {
|
|
229
|
+
this.#lastInstances = instances;
|
|
230
|
+
this.#snapTs = typeof ts === "number" ? ts : Date.now();
|
|
220
231
|
}
|
|
221
232
|
}
|
|
222
233
|
/** Vue POD agrégée, ou `null` tant qu'aucun snapshot n'est reçu (cold start). */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region nodefony/src/guards.ts
|
|
2
|
+
/**
|
|
3
|
+
* Gardes de type internes au paquet — elles remplacent une conversion (`as`)
|
|
4
|
+
* par une vérification faite à l'exécution, pour qu'une valeur d'une autre
|
|
5
|
+
* forme soit écartée au lieu de traverser le code sous un type qui ment.
|
|
6
|
+
*
|
|
7
|
+
* Non exportées par le paquet : ce sont des détails d'implémentation.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Vrai si la valeur est un objet non nul : ses propriétés se lisent alors,
|
|
11
|
+
* typées `unknown`, et chacune se vérifie avant usage.
|
|
12
|
+
*
|
|
13
|
+
* @param value - valeur venue du réseau, d'une métadonnée ou du conteneur
|
|
14
|
+
* @returns `true` pour tout objet (tableau compris), `false` pour `null` et les primitives
|
|
15
|
+
*/
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Vrai si la valeur est une fonction. Toute fonction JavaScript s'appelle avec
|
|
21
|
+
* n'importe quels arguments et rend une valeur inconnue : la signature annoncée
|
|
22
|
+
* ne promet rien de plus que ce que `typeof` a vérifié.
|
|
23
|
+
*
|
|
24
|
+
* @param value - valeur lue par son nom (métadonnée, propriété dynamique)
|
|
25
|
+
* @returns `true` si la valeur est appelable
|
|
26
|
+
*/
|
|
27
|
+
function isCallable(value) {
|
|
28
|
+
return typeof value === "function";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Vrai si la valeur se comporte comme une promesse (transport asynchrone) —
|
|
32
|
+
* un client Redis rend une promesse, un faux transport de test rend `void`.
|
|
33
|
+
*
|
|
34
|
+
* @param value - retour d'un transport
|
|
35
|
+
* @returns `true` si la valeur porte une méthode `then`
|
|
36
|
+
*/
|
|
37
|
+
function isThenable(value) {
|
|
38
|
+
return isRecord(value) && typeof value.then === "function";
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { isCallable, isRecord, isThenable };
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* token avec `isAuthenticated() === false` et appliqueront leur politique
|
|
13
13
|
* (Zero Trust : zone protégée → frame refusée par `beforeDispatch`).
|
|
14
14
|
*/
|
|
15
|
-
const anonymousAttributes = Object.freeze({});
|
|
16
15
|
const ANONYMOUS_REALTIME_TOKEN = Object.freeze({
|
|
17
16
|
type: "anonymous",
|
|
18
17
|
getUserIdentifier() {
|
|
@@ -27,9 +26,7 @@ const ANONYMOUS_REALTIME_TOKEN = Object.freeze({
|
|
|
27
26
|
getScopes() {
|
|
28
27
|
return [];
|
|
29
28
|
},
|
|
30
|
-
getAttribute(
|
|
31
|
-
return anonymousAttributes[key];
|
|
32
|
-
}
|
|
29
|
+
getAttribute(_key) {}
|
|
33
30
|
});
|
|
34
31
|
//#endregion
|
|
35
32
|
export { ANONYMOUS_REALTIME_TOKEN, ANONYMOUS_REALTIME_TOKEN as default };
|