@nodefony/realtime 10.0.0-beta.6 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +15 -3
- package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
- package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
- package/dist/nodefony/src/backplane/controlOrder.js +34 -5
- package/dist/nodefony/src/backplane/envelope.js +422 -45
- package/dist/nodefony/src/backplane/ingress.js +93 -0
- package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
- package/dist/nodefony/src/backplane/publishQueue.js +97 -3
- package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
- package/dist/nodefony/src/server/RealtimeController.js +87 -44
- package/dist/nodefony/src/server/RealtimeHub.js +631 -127
- package/dist/nodefony/src/server/admissionBudget.js +8 -8
- package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/contractRoutes.js +3 -13
- package/dist/nodefony/src/server/frameAdmission.js +27 -35
- package/dist/nodefony/src/server/presenceBus.js +486 -0
- package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
- package/dist/nodefony/src/server/remotePresence.js +541 -0
- package/dist/nodefony/src/server/subscribedAck.js +3 -0
- package/dist/nodefony/src/service/RealtimeService.js +9 -0
- package/dist/nodefony/testing/backplaneConformance.js +79 -3
- package/dist/nodefony/testing/index.js +22 -8
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
- package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
- package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
- package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
- package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
- package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
- package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
- package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
- package/dist/types/nodefony/testing/index.d.ts +1 -1
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +1 -0
- package/docs/architecture.md +21 -6
- package/docs/canaux-a-membres.md +13 -3
- package/docs/cas-d-usage.md +4 -3
- package/docs/configuration.md +38 -2
- package/docs/cookbook-chat.md +400 -152
- package/docs/observabilite.md +28 -9
- package/docs/protocole.md +10 -10
- package/docs/securite.md +64 -10
- package/package.json +12 -12
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { RealtimeConnection } from "./RealtimeConnection.js";
|
|
2
|
+
import { ChannelPresence, presenceDefect, presenceSettingsOf } from "./channelPresence.js";
|
|
3
|
+
import { PresenceBus } from "./presenceBus.js";
|
|
2
4
|
import { AdmissionBudget } from "./admissionBudget.js";
|
|
3
5
|
import { dropRetainedVerdicts } from "./frameAdmission.js";
|
|
4
|
-
import {
|
|
6
|
+
import { DEFAULT_ACTION_POLICY } from "../../decorators/realtimeDecorators.js";
|
|
7
|
+
import { USER_CHANNEL_DECLARATION, declarationOf, policyDeclarationsDefect, resolveDeclaredPolicies } from "./channelPolicyResolution.js";
|
|
5
8
|
import { LoopbackBackplane } from "../backplane/LoopbackBackplane.js";
|
|
6
|
-
import { readControlOrder } from "../backplane/controlOrder.js";
|
|
9
|
+
import { isUnknownControlOrder, readControlOrder } from "../backplane/controlOrder.js";
|
|
7
10
|
import { ANONYMOUS_REALTIME_TOKEN } from "./AnonymousRealtimeToken.js";
|
|
8
|
-
import { BootConfigurationError, IDENTITY_DEAD_CLOSE_CODE, IDENTITY_REFRESH_CLOSE_CODE, MAX_CHANNEL_PATTERN_WILDCARDS, NODEFONY_CHANNEL_NAMESPACE, PLATFORM_BUS_CHANNELS, REVOCATION_REVALIDATE_MS, RevocationWatch, TOKEN_EXPIRED_CLOSE_CODE, USER_CHANNEL_PREFIX, compileChannelPattern, containsBytes, countWildcards, frameCodecSet,
|
|
11
|
+
import { BootConfigurationError, IDENTITY_DEAD_CLOSE_CODE, IDENTITY_REFRESH_CLOSE_CODE, JsonRpcPeer, MAX_CHANNEL_PATTERN_WILDCARDS, NODEFONY_CHANNEL_NAMESPACE, PLATFORM_BUS_CHANNELS, REALTIME_PRESENCE_METHOD, REVOCATION_REVALIDATE_MS, RevocationWatch, TOKEN_EXPIRED_CLOSE_CODE, USER_CHANNEL_PREFIX, channelPatternWeight, compileChannelPattern, containsBytes, countWildcards, frameCodecSet, isPlatformBusChannel, isPlatformChannel, isRealtimePayload, realtimePayloadError, userChannel } from "nodefony";
|
|
9
12
|
import { frameRateRejectedTotal } from "@nodefony/http";
|
|
10
13
|
// nodefony/src/server/RealtimeHub.ts
|
|
11
14
|
/**
|
|
@@ -150,51 +153,29 @@ var ChannelFrame = class {
|
|
|
150
153
|
}
|
|
151
154
|
};
|
|
152
155
|
/**
|
|
153
|
-
* Règle d'admission prête à appeler d'une politique, ou `null` sans `allow`.
|
|
154
|
-
*
|
|
155
|
-
* Une flèche se retient telle quelle. Une classe exige son instance — résolue au
|
|
156
|
-
* démarrage, jamais ici : le hub n'a pas d'injecteur, et résoudre à la première
|
|
157
|
-
* poignée de main laisserait sans politique un exemplaire qui n'émet que des
|
|
158
|
-
* webhooks. La méthode est liée une fois, et la règle se construit une fois :
|
|
159
|
-
* le `subscribe` n'alloue rien et n'appelle qu'une fonction.
|
|
160
|
-
*/
|
|
161
|
-
function admissionRuleOf(name, policy, admission) {
|
|
162
|
-
const allow = policy.allow;
|
|
163
|
-
if (allow === void 0) return null;
|
|
164
|
-
const anonymous = policy.anonymous === true;
|
|
165
|
-
if (!isAdmissionClass(allow)) return {
|
|
166
|
-
allow,
|
|
167
|
-
anonymous
|
|
168
|
-
};
|
|
169
|
-
if (!(admission instanceof allow)) throw new BootConfigurationError(`canal temps réel « ${name} » : la classe d'admission ${allow.name} n'a pas été résolue au démarrage du noyau. Son contrôleur doit être déclaré par @controllers dans un module : c'est là que le framework l'inscrit et résout ses dépendances.`);
|
|
170
|
-
return {
|
|
171
|
-
allow: admission.allow.bind(admission),
|
|
172
|
-
anonymous
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Politique INTÉGRÉE du canal personnel `user:<id>` (ADR-0022 §6, D1) — connexion authentifiée, admise sur SON canal seulement.
|
|
177
|
-
*
|
|
178
|
-
* Constante de module : elle ne s'inscrit nulle part, donc aucun `clear()`,
|
|
179
|
-
* aucun ordre de démarrage, aucun oubli ne peut la retirer. Elle sert de REPLI :
|
|
180
|
-
* une politique que l'application écrit sur `user:…` (nom exact ou motif)
|
|
181
|
-
* l'emporte, comme toute déclaration explicite.
|
|
182
|
-
*/
|
|
183
|
-
const USER_CHANNEL_POLICY = Object.freeze({
|
|
184
|
-
policy: Object.freeze({
|
|
185
|
-
authenticated: true,
|
|
186
|
-
allow: isOwnUserChannel
|
|
187
|
-
}),
|
|
188
|
-
admission: Object.freeze({
|
|
189
|
-
allow: isOwnUserChannel,
|
|
190
|
-
anonymous: false
|
|
191
|
-
})
|
|
192
|
-
});
|
|
193
|
-
/**
|
|
194
156
|
* Fabrique intégrée du canal personnel : il n'a pas de source, seul `publish`
|
|
195
157
|
* l'alimente. Partagée par tous les canaux `user:` — aucune allocation par canal.
|
|
196
158
|
*/
|
|
197
159
|
const USER_CHANNEL_DISPOSE = () => {};
|
|
160
|
+
/** Identité sous laquelle une connexion figure dans la présence : nommée ⇔ compte local (ADR-0025 P3). */
|
|
161
|
+
function presenceKeyOf(member) {
|
|
162
|
+
return member.account ? member.userId : null;
|
|
163
|
+
}
|
|
164
|
+
/** Ensemble vide partagé : la présence répartie sans canal servi n'alloue rien. */
|
|
165
|
+
const EMPTY_PRESENCES = /* @__PURE__ */ new Set();
|
|
166
|
+
/** Un ensemble d'abonnés à un seul membre, sans le tableau temporaire de `new Set([sink])`. */
|
|
167
|
+
function singleSink(sink) {
|
|
168
|
+
const sinks = /* @__PURE__ */ new Set();
|
|
169
|
+
sinks.add(sink);
|
|
170
|
+
return sinks;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Prochain instant où la présence d'un canal a quelque chose à faire : la fin de la fenêtre si des changements attendent, la prochaine fin de grâce — `Infinity` sinon. O(1).
|
|
174
|
+
*/
|
|
175
|
+
function presenceDue(presence) {
|
|
176
|
+
const deadline = presence.nextDeadline();
|
|
177
|
+
return presence.dirty && presence.windowEnd < deadline ? presence.windowEnd : deadline;
|
|
178
|
+
}
|
|
198
179
|
/**
|
|
199
180
|
* RealtimeHub — broker temps réel **PAR INSTANCE** (1 pod = 1 process = 1 hub local).
|
|
200
181
|
*
|
|
@@ -224,6 +205,30 @@ const USER_CHANNEL_DISPOSE = () => {};
|
|
|
224
205
|
*/
|
|
225
206
|
var RealtimeHub = class RealtimeHub {
|
|
226
207
|
#channels = null;
|
|
208
|
+
#presenceActive = null;
|
|
209
|
+
#presenceTimer = null;
|
|
210
|
+
#presenceTimerAt = Number.POSITIVE_INFINITY;
|
|
211
|
+
#presenceBus = null;
|
|
212
|
+
#presenceServed = null;
|
|
213
|
+
/**
|
|
214
|
+
* Présences RÉPARTIES dont le dernier client local est parti pendant une grâce :
|
|
215
|
+
* encore servies au bus jusqu'à l'échéance, reprises si le canal se rouvre ici
|
|
216
|
+
* (ADR-0025 P5). `null` tant qu'aucune n'attend — le cas de presque tous les pods.
|
|
217
|
+
*/
|
|
218
|
+
#presenceLingering = null;
|
|
219
|
+
/** Publication remise aux fabriques de canal — UNE fermeture par hub, pas une par canal ouvert. */
|
|
220
|
+
#publishFn = (ch, payload) => {
|
|
221
|
+
this.publish(ch, payload);
|
|
222
|
+
};
|
|
223
|
+
/** Rappel du tic de présence — UNE fermeture par hub, pas une par armement. */
|
|
224
|
+
#presenceTickFn = () => {
|
|
225
|
+
this.#presenceTick();
|
|
226
|
+
};
|
|
227
|
+
#presenceDeclared = false;
|
|
228
|
+
#presenceRejectedTotal = 0;
|
|
229
|
+
#presenceListTotal = 0;
|
|
230
|
+
#presenceFrameTotal = 0;
|
|
231
|
+
#presenceListBuiltTotal = 0;
|
|
227
232
|
#publishTotal = 0;
|
|
228
233
|
#fanoutTotal = 0;
|
|
229
234
|
#closedBytesSent = 0;
|
|
@@ -233,6 +238,7 @@ var RealtimeHub = class RealtimeHub {
|
|
|
233
238
|
#closedMalformedDiscarded = 0;
|
|
234
239
|
#inboundTotal = 0;
|
|
235
240
|
#ingressRejectedTotal = 0;
|
|
241
|
+
#ingressUnknownDiscardedTotal = 0;
|
|
236
242
|
#localOnlyTotal = 0;
|
|
237
243
|
#systemFloorDeniedTotal = 0;
|
|
238
244
|
#notice = null;
|
|
@@ -252,12 +258,14 @@ var RealtimeHub = class RealtimeHub {
|
|
|
252
258
|
#channelProbe = null;
|
|
253
259
|
#retainers = null;
|
|
254
260
|
#channelPolicies = null;
|
|
261
|
+
#policyDeclarations = null;
|
|
255
262
|
/**
|
|
256
263
|
* Classes de contrôleur dont les politiques sont déjà inscrites — `null`
|
|
257
264
|
* tant qu'aucune ne l'est. Une poignée de main n'en paie qu'un `has` :
|
|
258
265
|
* l'inscription elle-même n'a lieu qu'une fois par classe.
|
|
259
266
|
*/
|
|
260
267
|
#policyOwners = null;
|
|
268
|
+
#policyClaims = null;
|
|
261
269
|
/**
|
|
262
270
|
* Politiques déclarées par MOTIF de nom (`chat:room:*`), triées du plus
|
|
263
271
|
* spécifique au plus général, et `null` tant qu'aucune n'est déclarée.
|
|
@@ -319,11 +327,116 @@ var RealtimeHub = class RealtimeHub {
|
|
|
319
327
|
if (!this.subscribe(channel, sink, factory, buildFrame)) return false;
|
|
320
328
|
if (member !== void 0) {
|
|
321
329
|
const st = this.#channels?.get(channel);
|
|
322
|
-
if (st !== void 0)
|
|
330
|
+
if (st !== void 0) {
|
|
331
|
+
if (st.members === null) {
|
|
332
|
+
st.members = /* @__PURE__ */ new Map();
|
|
333
|
+
const settings = presenceSettingsOf(this.resolveChannelPolicy(channel));
|
|
334
|
+
if (settings !== null) this.#openPresence(st, channel, settings);
|
|
335
|
+
}
|
|
336
|
+
st.members.set(sink, member);
|
|
337
|
+
const presence = st.presence;
|
|
338
|
+
if (presence !== null) {
|
|
339
|
+
presence.join(presenceKeyOf(member));
|
|
340
|
+
if (presence.distributed) this.#presenceBusChanged(presence);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
323
343
|
}
|
|
324
344
|
return true;
|
|
325
345
|
}
|
|
326
346
|
/**
|
|
347
|
+
* Ouvre la présence d'un canal à son premier client : répartie s'il est diffusé et qu'un bus est branché, locale sinon (avis unique : un bus est là, ce canal n'en profite pas).
|
|
348
|
+
*/
|
|
349
|
+
#openPresence(st, channel, settings) {
|
|
350
|
+
const backplane = this.#backplane;
|
|
351
|
+
const distributed = backplane !== null && st.forward;
|
|
352
|
+
const lingering = this.#presenceLingering?.get(channel);
|
|
353
|
+
if (lingering !== void 0 && distributed) {
|
|
354
|
+
this.#presenceLingering?.delete(channel);
|
|
355
|
+
if (this.#presenceLingering?.size === 0) this.#presenceLingering = null;
|
|
356
|
+
lingering.live = true;
|
|
357
|
+
st.presence = lingering;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const presence = new ChannelPresence(channel, settings, distributed);
|
|
361
|
+
st.presence = presence;
|
|
362
|
+
if (!distributed) {
|
|
363
|
+
if (backplane !== null) this.#notifyOnce("presence-local", `realtime: le canal « ${channel} » déclare la présence sans être diffusé entre serveurs (@RealtimeBroadcast) : sa présence ne compte que les connexions de CE serveur.`, "INFO");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const bus = this.#presenceBusFor(backplane);
|
|
367
|
+
(this.#presenceServed ??= /* @__PURE__ */ new Set()).add(presence);
|
|
368
|
+
const now = performance.now();
|
|
369
|
+
bus.served(presence, now);
|
|
370
|
+
this.#armPresenceAt(bus.due(), now);
|
|
371
|
+
}
|
|
372
|
+
/** La présence répartie de ce hub, créée au premier besoin. */
|
|
373
|
+
#presenceBusFor(backplane) {
|
|
374
|
+
const existing = this.#presenceBus;
|
|
375
|
+
if (existing !== null) return existing;
|
|
376
|
+
const bus = new PresenceBus({
|
|
377
|
+
originId: backplane.originId,
|
|
378
|
+
publish: (channel, payload) => backplane.publish(channel, payload),
|
|
379
|
+
lookup: (channel) => this.#presenceLookup(channel),
|
|
380
|
+
served: (channel) => {
|
|
381
|
+
const presence = this.#channels?.get(channel)?.presence;
|
|
382
|
+
return presence != null && presence.distributed && presence.live ? presence : null;
|
|
383
|
+
},
|
|
384
|
+
servedPresences: () => this.#presenceServed ?? EMPTY_PRESENCES,
|
|
385
|
+
servedCount: () => this.#presenceServed?.size ?? 0,
|
|
386
|
+
echoTotal: () => backplane.describe().echoTotal
|
|
387
|
+
});
|
|
388
|
+
this.#presenceBus = bus;
|
|
389
|
+
return bus;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Résout un canal nommé par un pair : sa présence s'il est servi et diffusé ici ; `null` s'il déclare la présence et se diffuse sans être servi (lu puis jeté) ; `false` sinon (refusé, compté).
|
|
393
|
+
*/
|
|
394
|
+
#presenceLookup(channel) {
|
|
395
|
+
const presence = this.#channels?.get(channel)?.presence;
|
|
396
|
+
if (presence != null && presence.distributed && presence.live) return presence;
|
|
397
|
+
if (!this.#isBroadcast(channel) || presenceSettingsOf(this.resolveChannelPolicy(channel)) === null) return false;
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
/** La part locale d'une présence répartie a changé : prochain lot armé. */
|
|
401
|
+
#presenceBusChanged(presence) {
|
|
402
|
+
const bus = this.#presenceBus;
|
|
403
|
+
if (bus === null) return;
|
|
404
|
+
const now = performance.now();
|
|
405
|
+
bus.changed(presence, now);
|
|
406
|
+
this.#armPresenceAt(bus.due(), now);
|
|
407
|
+
}
|
|
408
|
+
/** Rafraîchit les canaux que le bus a touchés (vue des pairs, état incomplet). */
|
|
409
|
+
#presenceTouchFromBus(bus, now) {
|
|
410
|
+
const touched = bus.takeTouched();
|
|
411
|
+
if (touched === null) return;
|
|
412
|
+
const channels = this.#channels;
|
|
413
|
+
for (const presence of touched) {
|
|
414
|
+
const st = channels?.get(presence.channel);
|
|
415
|
+
if (st === void 0 || st.presence !== presence) continue;
|
|
416
|
+
if (presence.busDirty) bus.changed(presence, now);
|
|
417
|
+
this.#presenceTouch(st, presence, now);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Entrée des deux canaux de présence du bus (ADR-0025 P2) — lots, demandes, réponses, preuve de vie.
|
|
422
|
+
*/
|
|
423
|
+
#admitPresence(msg, alive) {
|
|
424
|
+
let bus = this.#presenceBus;
|
|
425
|
+
if (bus === null) {
|
|
426
|
+
const backplane = this.#backplane;
|
|
427
|
+
if (!this.#presenceDeclared || backplane === null) {
|
|
428
|
+
this.#presenceRejectedTotal += 1;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
bus = this.#presenceBusFor(backplane);
|
|
432
|
+
}
|
|
433
|
+
const now = performance.now();
|
|
434
|
+
if (alive) bus.receiveAlive(msg.originId, msg.payload, now);
|
|
435
|
+
else if (bus.receive(msg.originId, msg.payload, now)) this.#discardUnknownFromBus("presence");
|
|
436
|
+
this.#presenceTouchFromBus(bus, now);
|
|
437
|
+
this.#armPresenceAt(bus.due(), now);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
327
440
|
* Abonne un consommateur qui **répond du canal** : il apporte la `factory`, donc
|
|
328
441
|
* la réponse à « ce canal existe-t-il, et qui le produit ? ». Le provider partagé
|
|
329
442
|
* est créé au 1ᵉʳ abonné de ce genre. Le sink est inscrit AVANT l'appel à
|
|
@@ -359,8 +472,9 @@ var RealtimeHub = class RealtimeHub {
|
|
|
359
472
|
st = {
|
|
360
473
|
dispose: null,
|
|
361
474
|
owned: false,
|
|
362
|
-
sinks:
|
|
475
|
+
sinks: singleSink(sink),
|
|
363
476
|
members: null,
|
|
477
|
+
presence: null,
|
|
364
478
|
buildFrame: buildFrame ?? null,
|
|
365
479
|
messages: 0,
|
|
366
480
|
forward: this.#isBroadcast(channel)
|
|
@@ -399,8 +513,9 @@ var RealtimeHub = class RealtimeHub {
|
|
|
399
513
|
channels.set(channel, {
|
|
400
514
|
dispose: null,
|
|
401
515
|
owned: false,
|
|
402
|
-
sinks:
|
|
516
|
+
sinks: singleSink(sink),
|
|
403
517
|
members: null,
|
|
518
|
+
presence: null,
|
|
404
519
|
buildFrame: null,
|
|
405
520
|
messages: 0,
|
|
406
521
|
forward: this.#isBroadcast(channel)
|
|
@@ -418,7 +533,7 @@ var RealtimeHub = class RealtimeHub {
|
|
|
418
533
|
* canaux SYSTÈME en dernier recours. `null` = canal inconnu des deux.
|
|
419
534
|
*/
|
|
420
535
|
#createProvider(channel, factory) {
|
|
421
|
-
const publishFn =
|
|
536
|
+
const publishFn = this.#publishFn;
|
|
422
537
|
const dispose = factory(channel, publishFn);
|
|
423
538
|
if (dispose !== null) return dispose;
|
|
424
539
|
const sys = this.#systemChannelFactories?.get(channel);
|
|
@@ -434,15 +549,190 @@ var RealtimeHub = class RealtimeHub {
|
|
|
434
549
|
const st = channels?.get(channel);
|
|
435
550
|
if (!channels || !st) return;
|
|
436
551
|
st.sinks.delete(sink);
|
|
552
|
+
const presence = st.presence;
|
|
553
|
+
if (presence !== null) {
|
|
554
|
+
const member = st.members?.get(sink);
|
|
555
|
+
if (member !== void 0) {
|
|
556
|
+
const now = performance.now();
|
|
557
|
+
presence.leave(presenceKeyOf(member), now);
|
|
558
|
+
this.#presenceTouch(st, presence, now);
|
|
559
|
+
if (presence.distributed) this.#presenceBusChanged(presence);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
437
562
|
st.members?.delete(sink);
|
|
438
563
|
if (st.sinks.size === 0) {
|
|
439
564
|
try {
|
|
440
565
|
st.dispose?.();
|
|
441
566
|
} catch {}
|
|
442
567
|
channels.delete(channel);
|
|
568
|
+
if (presence !== null) {
|
|
569
|
+
presence.live = false;
|
|
570
|
+
if (!presence.distributed) {} else if (presence.hasLocalGrace) this.#lingerPresence(presence);
|
|
571
|
+
else this.#releasePresence(presence);
|
|
572
|
+
}
|
|
443
573
|
}
|
|
444
574
|
}
|
|
445
575
|
/**
|
|
576
|
+
* Le dernier client local d'une présence répartie est parti pendant une grâce : elle reste servie au bus jusqu'à l'échéance (ADR-0025 P5) — le départ n'est publié qu'à ce moment, et un retour sur ce pod l'annule.
|
|
577
|
+
*/
|
|
578
|
+
#lingerPresence(presence) {
|
|
579
|
+
(this.#presenceLingering ??= /* @__PURE__ */ new Map()).set(presence.channel, presence);
|
|
580
|
+
this.#armPresenceAt(presence.nextDeadline(), performance.now());
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Un canal à présence répartie n'est plus servi ici : sa part locale part à zéro au prochain lot (aucun fantôme chez les pairs), sa vue des pairs est jetée.
|
|
584
|
+
*/
|
|
585
|
+
#releasePresence(presence) {
|
|
586
|
+
this.#presenceServed?.delete(presence);
|
|
587
|
+
const bus = this.#presenceBus;
|
|
588
|
+
if (bus === null) return;
|
|
589
|
+
bus.remote.dropChannel(presence.channel);
|
|
590
|
+
presence.release();
|
|
591
|
+
this.#presenceBusChanged(presence);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Présence d'un canal vue de CE pod (ADR-0025 P3) — `count`, `unnamed`, `members` (toujours, quel que soit le seuil de la trame), `partial`.
|
|
595
|
+
*
|
|
596
|
+
* La voie de l'observateur qui lit sans figurer (modérateur, tableau de
|
|
597
|
+
* bord). Chemin froid : une copie à chaque appel.
|
|
598
|
+
*
|
|
599
|
+
* @param channel - nom exact du canal.
|
|
600
|
+
* @returns la vue (vide si personne n'est là), ou `null` si la politique du
|
|
601
|
+
* canal ne déclare pas `presence`.
|
|
602
|
+
*/
|
|
603
|
+
presence(channel) {
|
|
604
|
+
const live = this.#channels?.get(channel)?.presence;
|
|
605
|
+
if (live != null) return live.view();
|
|
606
|
+
if (presenceSettingsOf(this.resolveChannelPolicy(channel)) === null) return null;
|
|
607
|
+
return {
|
|
608
|
+
count: 0,
|
|
609
|
+
unnamed: 0,
|
|
610
|
+
members: /* @__PURE__ */ new Map(),
|
|
611
|
+
partial: false
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Arrêt propre de la présence répartie (ADR-0025 P5) — le dernier lot part, puis une preuve de vie FINALE : les pairs retirent aussitôt les présents de ce serveur, sans attendre le délai d'oubli. Plus rien ne part ensuite ; idempotent, sans effet sans bus.
|
|
616
|
+
*
|
|
617
|
+
* Appelé par le module à l'arrêt du noyau (`onTerminate`), AVANT que le bus ne
|
|
618
|
+
* se ferme. Les connexions qui se ferment ensuite ne publient plus rien : la
|
|
619
|
+
* preuve de vie finale les a déjà retirées chez les pairs.
|
|
620
|
+
*/
|
|
621
|
+
endPresence() {
|
|
622
|
+
const bus = this.#presenceBus;
|
|
623
|
+
if (bus === null) return;
|
|
624
|
+
bus.end();
|
|
625
|
+
if (this.#presenceTimer !== null) clearTimeout(this.#presenceTimer);
|
|
626
|
+
this.#presenceTimer = null;
|
|
627
|
+
this.#presenceTimerAt = Number.POSITIVE_INFINITY;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Un abonnement vient d'être acquitté : sur un canal à présence, la liste complète part vers CETTE connexion, puis les changements en attente partent si la fenêtre le permet.
|
|
631
|
+
*
|
|
632
|
+
* Appelé par le contrôleur APRÈS `realtime:subscribed`, à chaque (re-)`subscribe`
|
|
633
|
+
* — aucune borne : la contre-pression peut avoir jeté la liste d'avant sans
|
|
634
|
+
* le dire, et c'est le re-`subscribe` qui répare (ADR-0025 P2 bis). La liste
|
|
635
|
+
* est encodée une fois par envoi de changements et par codec, puis servie
|
|
636
|
+
* telle quelle.
|
|
637
|
+
*
|
|
638
|
+
* @param channel - nom exact du canal acquitté.
|
|
639
|
+
* @param connection - la connexion abonnée.
|
|
640
|
+
*/
|
|
641
|
+
presenceSubscribed(channel, connection) {
|
|
642
|
+
const st = this.#channels?.get(channel);
|
|
643
|
+
const presence = st?.presence;
|
|
644
|
+
if (st === void 0 || presence == null) return;
|
|
645
|
+
if (presence.listFrame === null) {
|
|
646
|
+
presence.listFrame = new ChannelFrame(JsonRpcPeer.buildNotification(REALTIME_PRESENCE_METHOD, presence.listParams()));
|
|
647
|
+
this.#presenceListBuiltTotal += 1;
|
|
648
|
+
}
|
|
649
|
+
this.#presenceListTotal += 1;
|
|
650
|
+
connection.deliver(presence.listFrame);
|
|
651
|
+
this.#presenceTouch(st, presence, performance.now());
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Un changement de présence vient d'avoir lieu : part tout de suite sur un canal calme (front montant), sinon au tic qui ferme la fenêtre.
|
|
655
|
+
*/
|
|
656
|
+
#presenceTouch(st, presence, now) {
|
|
657
|
+
if (presence.dirty && now >= presence.windowEnd) this.#presenceFlush(st, presence, now);
|
|
658
|
+
const due = presenceDue(presence);
|
|
659
|
+
if (due === 1e400) return;
|
|
660
|
+
(this.#presenceActive ??= /* @__PURE__ */ new Set()).add(st);
|
|
661
|
+
this.#armPresenceAt(due, now);
|
|
662
|
+
}
|
|
663
|
+
/** Envoie les changements en attente à tous les membres, et ouvre une fenêtre. */
|
|
664
|
+
#presenceFlush(st, presence, now) {
|
|
665
|
+
const params = presence.takeChanges();
|
|
666
|
+
if (params === null) return;
|
|
667
|
+
presence.windowEnd = now + 250;
|
|
668
|
+
const members = st.members;
|
|
669
|
+
if (members === null || members.size === 0) return;
|
|
670
|
+
const frame = new ChannelFrame(JsonRpcPeer.buildNotification(REALTIME_PRESENCE_METHOD, params));
|
|
671
|
+
this.#presenceFrameTotal += 1;
|
|
672
|
+
for (const member of members.values()) try {
|
|
673
|
+
member.deliver(frame);
|
|
674
|
+
} catch {}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Avance le minuteur UNIQUE de la présence à `at` s'il est plus proche que l'échéance déjà armée — O(1) par changement, aucun parcours des canaux, aucun minuteur par canal ni par présent.
|
|
678
|
+
*/
|
|
679
|
+
#armPresenceAt(next, now) {
|
|
680
|
+
if (this.#presenceTimer !== null && this.#presenceTimerAt <= next) return;
|
|
681
|
+
if (this.#presenceTimer !== null) clearTimeout(this.#presenceTimer);
|
|
682
|
+
this.#presenceTimerAt = next;
|
|
683
|
+
this.#presenceTimer = setTimeout(this.#presenceTickFn, Math.max(0, Math.ceil(next - now)));
|
|
684
|
+
this.#presenceTimer.unref();
|
|
685
|
+
}
|
|
686
|
+
/** Tic unique : grâces écoulées, fenêtres échues ; se retire quand plus rien n'attend. */
|
|
687
|
+
#presenceTick() {
|
|
688
|
+
this.#presenceTimer = null;
|
|
689
|
+
this.#presenceTimerAt = Number.POSITIVE_INFINITY;
|
|
690
|
+
const now = performance.now();
|
|
691
|
+
const bus = this.#presenceBus;
|
|
692
|
+
if (bus !== null) {
|
|
693
|
+
bus.tick(now);
|
|
694
|
+
this.#presenceTouchFromBus(bus, now);
|
|
695
|
+
}
|
|
696
|
+
let next = Number.POSITIVE_INFINITY;
|
|
697
|
+
const active = this.#presenceActive;
|
|
698
|
+
if (active !== null) {
|
|
699
|
+
for (const st of active) {
|
|
700
|
+
const presence = st.presence;
|
|
701
|
+
if (presence === null || !presence.live) {
|
|
702
|
+
active.delete(st);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
presence.expire(now);
|
|
706
|
+
if (bus !== null && presence.busDirty) bus.changed(presence, now);
|
|
707
|
+
if (presence.dirty && now >= presence.windowEnd) this.#presenceFlush(st, presence, now);
|
|
708
|
+
const due = presenceDue(presence);
|
|
709
|
+
if (due === 1e400) active.delete(st);
|
|
710
|
+
else if (due < next) next = due;
|
|
711
|
+
}
|
|
712
|
+
if (active.size === 0) this.#presenceActive = null;
|
|
713
|
+
}
|
|
714
|
+
const lingering = this.#presenceLingering;
|
|
715
|
+
if (lingering !== null) {
|
|
716
|
+
for (const presence of lingering.values()) {
|
|
717
|
+
presence.expire(now);
|
|
718
|
+
if (bus !== null && presence.busDirty) bus.changed(presence, now);
|
|
719
|
+
if (presence.hasLocalGrace) {
|
|
720
|
+
const due = presence.nextDeadline();
|
|
721
|
+
if (due < next) next = due;
|
|
722
|
+
} else {
|
|
723
|
+
lingering.delete(presence.channel);
|
|
724
|
+
this.#releasePresence(presence);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (lingering.size === 0) this.#presenceLingering = null;
|
|
728
|
+
}
|
|
729
|
+
if (bus !== null) {
|
|
730
|
+
const due = bus.due();
|
|
731
|
+
if (due < next) next = due;
|
|
732
|
+
}
|
|
733
|
+
if (next !== Number.POSITIVE_INFINITY) this.#armPresenceAt(next, now);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
446
736
|
* Les connexions abonnées à un canal par un client — **local à ce pod** : les membres servis par un autre exemplaire n'y figurent pas.
|
|
447
737
|
*
|
|
448
738
|
* Les écouteurs et services du serveur (`listen`, `subscribe`) ne sont pas
|
|
@@ -569,7 +859,12 @@ var RealtimeHub = class RealtimeHub {
|
|
|
569
859
|
/** Applique un ordre reçu du bus : forme vérifiée, canal de la plateforme refusé, sinon compté en rejet. */
|
|
570
860
|
#applyControl(payload) {
|
|
571
861
|
const order = readControlOrder(payload);
|
|
572
|
-
if (order === null
|
|
862
|
+
if (order === null) {
|
|
863
|
+
if (isUnknownControlOrder(payload)) this.#discardUnknownFromBus("order");
|
|
864
|
+
else this.#ingressRejectedTotal += 1;
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (isReservedSystemChannel(order.channel)) {
|
|
573
868
|
this.#ingressRejectedTotal += 1;
|
|
574
869
|
return;
|
|
575
870
|
}
|
|
@@ -722,14 +1017,23 @@ var RealtimeHub = class RealtimeHub {
|
|
|
722
1017
|
if (prefixes.includes(prefix)) return;
|
|
723
1018
|
prefixes.push(prefix);
|
|
724
1019
|
if (this.#channels) {
|
|
725
|
-
for (const [channel, st] of this.#channels) if (!st.forward && channel.startsWith(prefix)) st.forward = true;
|
|
1020
|
+
for (const [channel, st] of this.#channels) if (!st.forward && channel.startsWith(prefix) && !isPlatformChannel(channel)) st.forward = true;
|
|
726
1021
|
}
|
|
727
1022
|
}
|
|
728
|
-
/**
|
|
1023
|
+
/**
|
|
1024
|
+
* Un canal est-il broadcast (préfixe déclaré) ? `false` si aucune politique (défaut local).
|
|
1025
|
+
*
|
|
1026
|
+
* Un canal de PLATEFORME ne l'est jamais, quel que soit le préfixe : un
|
|
1027
|
+
* préfixe large (`""`, `"n"`) le couvrirait, et {@link markBroadcastChannel}
|
|
1028
|
+
* ne refuse que le préfixe réservé lui-même. Sans cette exclusion, ses
|
|
1029
|
+
* publications sortaient sur le bus et l'entrée acceptait de faux évènements
|
|
1030
|
+
* d'admin venus d'un pair (F83). Le test n'est payé qu'après une
|
|
1031
|
+
* correspondance de préfixe.
|
|
1032
|
+
*/
|
|
729
1033
|
#isBroadcast(channel) {
|
|
730
1034
|
const prefixes = this.#broadcastPrefixes;
|
|
731
1035
|
if (prefixes === null) return false;
|
|
732
|
-
for (const prefix of prefixes) if (channel.startsWith(prefix)) return
|
|
1036
|
+
for (const prefix of prefixes) if (channel.startsWith(prefix)) return !isPlatformChannel(channel);
|
|
733
1037
|
return false;
|
|
734
1038
|
}
|
|
735
1039
|
/**
|
|
@@ -744,6 +1048,9 @@ var RealtimeHub = class RealtimeHub {
|
|
|
744
1048
|
*/
|
|
745
1049
|
setBackplane(backplane) {
|
|
746
1050
|
this.#backplane = backplane;
|
|
1051
|
+
this.#presenceBus = null;
|
|
1052
|
+
this.#presenceServed = null;
|
|
1053
|
+
this.#presenceLingering = null;
|
|
747
1054
|
this.markBroadcastChannel(USER_CHANNEL_PREFIX);
|
|
748
1055
|
backplane.onMessage((msg) => this.#admitFromBackplane(msg));
|
|
749
1056
|
this.#armBusBytes();
|
|
@@ -775,18 +1082,50 @@ var RealtimeHub = class RealtimeHub {
|
|
|
775
1082
|
* Seule exception, AVANT ce contrôle : le canal de contrôle
|
|
776
1083
|
* (`PLATFORM_BUS_CHANNELS.control`), jamais diffusé aux abonnés — ses ordres,
|
|
777
1084
|
* en liste fermée, sont appliqués aux connexions de ce pod ou comptés en rejet.
|
|
1085
|
+
* Puis, avant le contrôle aussi : un AUTRE canal du territoire `nodefony:bus:`
|
|
1086
|
+
* vient d'une version plus récente — jeté et compté à part
|
|
1087
|
+
* ({@link #discardUnknownFromBus}), jamais comme une injection.
|
|
778
1088
|
*/
|
|
779
1089
|
#admitFromBackplane(msg) {
|
|
1090
|
+
const st = this.#channels?.get(msg.channel);
|
|
1091
|
+
if (st?.forward === true) {
|
|
1092
|
+
this.#fanout(st, msg.payload);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
780
1095
|
if (msg.channel === PLATFORM_BUS_CHANNELS.control) {
|
|
781
1096
|
this.#applyControl(msg.payload);
|
|
782
1097
|
return;
|
|
783
1098
|
}
|
|
1099
|
+
if (isPlatformBusChannel(msg.channel)) {
|
|
1100
|
+
if (msg.channel === PLATFORM_BUS_CHANNELS.presence) this.#admitPresence(msg, false);
|
|
1101
|
+
else if (msg.channel === PLATFORM_BUS_CHANNELS.presenceAlive) this.#admitPresence(msg, true);
|
|
1102
|
+
else this.#discardUnknownFromBus("channel");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
784
1105
|
if (!this.#isBroadcast(msg.channel)) {
|
|
785
1106
|
this.#ingressRejectedTotal += 1;
|
|
786
1107
|
return;
|
|
787
1108
|
}
|
|
788
1109
|
this.publishLocal(msg.channel, msg.payload);
|
|
789
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Jette un message du bus venu d'une version plus RÉCENTE — canal `nodefony:bus:` ou type d'ordre inconnus — et le compte à part.
|
|
1113
|
+
*
|
|
1114
|
+
* Règle de lecture figée par l'ADR-0025 (S-rt-92) : pendant un déploiement
|
|
1115
|
+
* progressif, un serveur ancien reçoit à la cadence du message ce qu'une
|
|
1116
|
+
* version suivante ajoute au bus. Le compter dans `#ingressRejectedTotal`
|
|
1117
|
+
* déclencherait de fausses alertes d'injection et noierait la vraie. Rien
|
|
1118
|
+
* n'atteint un abonné ; le journal ne parle qu'une fois par motif (pas une
|
|
1119
|
+
* ligne par message), sans recopier le nom reçu — une donnée venue du bus
|
|
1120
|
+
* n'entre pas dans le journal.
|
|
1121
|
+
*
|
|
1122
|
+
* @param kind - `channel` (canal inconnu) ou `order` (type d'ordre inconnu).
|
|
1123
|
+
*/
|
|
1124
|
+
#discardUnknownFromBus(kind) {
|
|
1125
|
+
this.#ingressUnknownDiscardedTotal += 1;
|
|
1126
|
+
const what = kind === "channel" ? "un canal de la plateforme inconnu" : kind === "order" ? "un ordre de contrôle d'un type inconnu" : "un message de présence d'un type inconnu";
|
|
1127
|
+
this.#notifyOnce(`backplane:unknown-${kind}`, `realtime: ${what} de cette version est arrivé par le bus — ignoré (un serveur plus récent pendant un déploiement ?), compté dans ingressUnknownDiscardedTotal`, "INFO");
|
|
1128
|
+
}
|
|
790
1129
|
/** Backplane cross-process branché, ou `null` en mono-process (lecture/tests/sonde). */
|
|
791
1130
|
get backplane() {
|
|
792
1131
|
return this.#backplane;
|
|
@@ -911,6 +1250,7 @@ var RealtimeHub = class RealtimeHub {
|
|
|
911
1250
|
fanoutTotal: this.#fanoutTotal,
|
|
912
1251
|
inboundTotal: this.#inboundTotal,
|
|
913
1252
|
ingressRejectedTotal: this.#ingressRejectedTotal,
|
|
1253
|
+
ingressUnknownDiscardedTotal: this.#ingressUnknownDiscardedTotal,
|
|
914
1254
|
localOnlyTotal: this.#localOnlyTotal,
|
|
915
1255
|
systemFloorDeniedTotal: this.#systemFloorDeniedTotal,
|
|
916
1256
|
sealRejectedTotal: backplane.sealRejectedTotal ?? 0,
|
|
@@ -929,7 +1269,38 @@ var RealtimeHub = class RealtimeHub {
|
|
|
929
1269
|
slowConsumers,
|
|
930
1270
|
drops
|
|
931
1271
|
},
|
|
932
|
-
backplane
|
|
1272
|
+
backplane,
|
|
1273
|
+
presence: this.#presenceProbe()
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/** Compteurs de la présence (chemin froid : un parcours des canaux ouverts). */
|
|
1277
|
+
#presenceProbe() {
|
|
1278
|
+
let channels = 0;
|
|
1279
|
+
if (this.#channels !== null) {
|
|
1280
|
+
for (const st of this.#channels.values()) if (st.presence !== null) channels += 1;
|
|
1281
|
+
}
|
|
1282
|
+
const bus = this.#presenceBus;
|
|
1283
|
+
const remote = bus?.remote;
|
|
1284
|
+
return {
|
|
1285
|
+
channels,
|
|
1286
|
+
listTotal: this.#presenceListTotal,
|
|
1287
|
+
listBuiltTotal: this.#presenceListBuiltTotal,
|
|
1288
|
+
frameTotal: this.#presenceFrameTotal,
|
|
1289
|
+
distributedChannels: this.#presenceServed?.size ?? 0,
|
|
1290
|
+
pods: remote?.size ?? 0,
|
|
1291
|
+
gaps: remote?.gaps ?? 0,
|
|
1292
|
+
gapTotal: remote?.gapTotal ?? 0,
|
|
1293
|
+
forgottenTotal: remote?.forgottenTotal ?? 0,
|
|
1294
|
+
presenceRejectedTotal: this.#presenceRejectedTotal + (remote?.rejectedTotal ?? 0),
|
|
1295
|
+
presenceStaleDiscardedTotal: remote?.staleTotal ?? 0,
|
|
1296
|
+
batchTotal: bus?.batchTotal ?? 0,
|
|
1297
|
+
requestTotal: bus?.requestTotal ?? 0,
|
|
1298
|
+
answerTotal: bus?.answerTotal ?? 0,
|
|
1299
|
+
aliveTotal: bus?.aliveTotal ?? 0,
|
|
1300
|
+
endedTotal: remote?.endedTotal ?? 0,
|
|
1301
|
+
busLost: bus?.busLost ?? false,
|
|
1302
|
+
busLostTotal: bus?.busLostTotal ?? 0,
|
|
1303
|
+
rearmTotal: bus?.rearmTotal ?? 0
|
|
933
1304
|
};
|
|
934
1305
|
}
|
|
935
1306
|
/** Canaux actifs (≥ 1 abonné). Lecture seule. */
|
|
@@ -1056,8 +1427,16 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1056
1427
|
* **Seam #1 (P13 → P6)** — pose le verrou de frame (cf {@link FrameAuthorizer}).
|
|
1057
1428
|
* `null` retire la politique (bypass total). Posé 1× au boot par
|
|
1058
1429
|
* `@nodefony/security` depuis les zones `realtime: true`. Cold path.
|
|
1430
|
+
*
|
|
1431
|
+
* @throws BootConfigurationError Le verrou ne déclare pas `judgesWrite`
|
|
1432
|
+
* alors qu'une écriture distincte de la lecture est inscrite : il la
|
|
1433
|
+
* laisserait ouverte.
|
|
1059
1434
|
*/
|
|
1060
1435
|
setFrameAuthorizer(authorizer, options) {
|
|
1436
|
+
if (authorizer !== null && authorizer.judgesWrite !== true) {
|
|
1437
|
+
const defect = policyDeclarationsDefect(this.#policyDeclarations, this.#channelPolicyPatterns, false);
|
|
1438
|
+
if (defect !== null) throw new BootConfigurationError(defect);
|
|
1439
|
+
}
|
|
1061
1440
|
this.#frameAuthorizer = authorizer;
|
|
1062
1441
|
this.#dropRetainedVerdicts();
|
|
1063
1442
|
this.#channelProbe = options?.silentProbe ?? null;
|
|
@@ -1123,10 +1502,7 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1123
1502
|
return this.hasChannelPolicies() && this.#frameAuthorizer === null;
|
|
1124
1503
|
}
|
|
1125
1504
|
/**
|
|
1126
|
-
* **Seam #1b** —
|
|
1127
|
-
* (`@RealtimeChannel`/`@RealtimeInbound` avec opts). Appelé au handshake par
|
|
1128
|
-
* le controller pour CHAQUE canal décoré (idempotent : même nom = écrase, les
|
|
1129
|
-
* controllers d'un même endpoint déclarent la même policy). Cold path.
|
|
1505
|
+
* **Seam #1b** — pose la politique d'un canal/méthode en REMPLAÇANT ce que ce nom déclarait (voie programmatique ; les décorateurs passent par {@link RealtimeHub.declareChannelPolicy}). Cold path.
|
|
1130
1506
|
*
|
|
1131
1507
|
* Deux formes de nom, et la seconde garde une FAMILLE :
|
|
1132
1508
|
*
|
|
@@ -1136,35 +1512,124 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1136
1512
|
* Le `*` remplace n'importe quelle suite de caractères ; tout le reste du
|
|
1137
1513
|
* motif est littéral (échappé), y compris `.` et `(`.
|
|
1138
1514
|
*
|
|
1139
|
-
* Précédence
|
|
1140
|
-
*
|
|
1141
|
-
* **
|
|
1515
|
+
* Précédence, SORTE PAR SORTE (ADR-0022 §6, décision 1 quater) : la lecture
|
|
1516
|
+
* d'un nom vient de la déclaration la plus spécifique qui POSE une lecture —
|
|
1517
|
+
* le nom **exact**, puis le motif au littéral le plus long ; l'écriture de
|
|
1518
|
+
* même. C'est ainsi qu'on ouvre une exception dans une famille gardée, sans
|
|
1519
|
+
* effacer la sorte que l'exception ne déclare pas.
|
|
1142
1520
|
*
|
|
1143
|
-
* @param name
|
|
1521
|
+
* @param name - nom exact du canal, de la méthode entrante ou de l'action, ou
|
|
1144
1522
|
* MOTIF de famille s'il contient `*`.
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
* par l'injecteur, dont la méthode est liée ICI, une fois — le `subscribe`
|
|
1148
|
-
* n'appelle qu'une fonction prête.
|
|
1149
|
-
*
|
|
1150
|
-
* @param policy - exigences ({@link IChannelPolicy}) lues par security.
|
|
1523
|
+
* @param policy - exigences de LECTURE ({@link IChannelPolicy}) lues par
|
|
1524
|
+
* security ; seules exigées au `subscribe`.
|
|
1151
1525
|
* @param admission - instance de la classe d'admission quand `policy.allow`
|
|
1152
1526
|
* en est une ; ignorée sinon.
|
|
1153
|
-
* @
|
|
1154
|
-
*
|
|
1527
|
+
* @param write - exigences d'ÉCRITURE, exigées EN PLUS de la lecture sur une
|
|
1528
|
+
* trame entrante ; `null` : ce nom n'en déclare pas.
|
|
1529
|
+
* @throws BootConfigurationError Classe d'admission sans instance ; `allow`
|
|
1530
|
+
* sur une écriture qui rencontre une lecture différente ; écriture distincte
|
|
1531
|
+
* face à un verrou qui ne sait pas la juger.
|
|
1155
1532
|
*/
|
|
1156
|
-
registerChannelPolicy(name, policy, admission) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1533
|
+
registerChannelPolicy(name, policy, admission, write = null) {
|
|
1534
|
+
this.#declare(name, policy, write, null, admission, false);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Déclare la lecture et/ou l'écriture d'un nom en les AJOUTANT à ce qu'il déclarait — une sorte posée remplace la sienne, une sorte `null` garde celle d'avant. Voie des décorateurs (`channelPolicyRegistry.ts#registerControllerPolicies`). Cold path.
|
|
1538
|
+
*
|
|
1539
|
+
* @param name - nom exact ou motif.
|
|
1540
|
+
* @param read - lecture déclarée, ou `null`.
|
|
1541
|
+
* @param write - écriture déclarée, ou `null`.
|
|
1542
|
+
* @param admission - instance de la classe d'admission désignée par l'une ou
|
|
1543
|
+
* l'autre ; ignorée sinon.
|
|
1544
|
+
* @throws BootConfigurationError Mêmes refus que {@link RealtimeHub.registerChannelPolicy}.
|
|
1545
|
+
*/
|
|
1546
|
+
declareChannelPolicy(name, read, write, admission) {
|
|
1547
|
+
this.#declare(name, read, write, null, admission, true);
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Pose le défaut FERMÉ d'une action (`{ authenticated: true }`) — son ÉCRITURE tant que rien d'exact ne déclare son nom et qu'aucun motif ne pose d'écriture ; une lecture de motif s'y ajoute sans l'ouvrir. Idempotent : une poignée de main n'en paie qu'une lecture.
|
|
1551
|
+
*
|
|
1552
|
+
* @param name - nom de l'action.
|
|
1553
|
+
*/
|
|
1554
|
+
declareActionDefault(name) {
|
|
1555
|
+
if (this.#policyDeclarations?.get(name)?.fallback === DEFAULT_ACTION_POLICY) return;
|
|
1556
|
+
this.#declare(name, null, null, DEFAULT_ACTION_POLICY, void 0, true);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Refuse une présence mal déclarée (ADR-0025 P3) : valeur illisible, posée sur une ÉCRITURE seule (rien ne s'y abonne), ou sur un canal réservé de la plateforme.
|
|
1560
|
+
*
|
|
1561
|
+
* Une seule porte pour les décorateurs et la voie programmatique. Une
|
|
1562
|
+
* politique posée par `@RealtimePolicy` sert la lecture ET l'écriture sous le
|
|
1563
|
+
* même objet : elle est admise, c'est la lecture qui la porte.
|
|
1564
|
+
*
|
|
1565
|
+
* @throws BootConfigurationError En nommant le canal et le défaut.
|
|
1566
|
+
*/
|
|
1567
|
+
static #assertPresence(name, read, write) {
|
|
1568
|
+
const refuse = (why) => {
|
|
1569
|
+
throw new BootConfigurationError(`canal temps réel « ${name} » : présence refusée — ${why}`);
|
|
1160
1570
|
};
|
|
1161
|
-
|
|
1571
|
+
for (const policy of [read, write]) {
|
|
1572
|
+
const defect = presenceDefect(policy?.presence);
|
|
1573
|
+
if (defect !== null) refuse(defect);
|
|
1574
|
+
}
|
|
1575
|
+
if (write !== null && write !== read && presenceSettingsOf(write) !== null) refuse("« presence » est posée sur une politique d'ÉCRITURE (@RealtimeInbound, @RealtimeAction), où rien ne s'abonne. La poser sur @RealtimeChannel.");
|
|
1576
|
+
if (presenceSettingsOf(read) !== null && isReservedSystemChannel(name)) refuse(`un canal réservé de la plateforme (${RESERVED_SYSTEM_PREFIXES.join(" ")}) ne publie pas sa présence.`);
|
|
1577
|
+
}
|
|
1578
|
+
/** Construit la déclaration d'un nom (remplacée ou fusionnée), puis la valide et la publie. */
|
|
1579
|
+
#declare(name, read, write, fallback, admission, merge) {
|
|
1162
1580
|
if (isReservedSystemChannel(name)) this.#notifyOnce("reserved-policy", `le canal "${name}" est déclaré dans un namespace RÉSERVÉ à la plateforme (${RESERVED_SYSTEM_PREFIXES.join(" ")}) : le plancher système s'y applique et exigera un rôle d'administration, quelle que soit la politique déclarée ici. Renommer le canal avec un préfixe applicatif si ce n'est pas voulu.`);
|
|
1163
|
-
|
|
1164
|
-
|
|
1581
|
+
const isPattern = name.includes("*");
|
|
1582
|
+
const previous = isPattern ? this.#channelPolicyPatterns?.find((p) => p.source === name) : this.#policyDeclarations?.get(name);
|
|
1583
|
+
const declaration = declarationOf(name, read ?? (merge ? previous?.read ?? null : null), write ?? (merge ? previous?.write ?? null : null), fallback ?? previous?.fallback ?? null, admission ?? previous?.instance);
|
|
1584
|
+
RealtimeHub.#assertPresence(name, declaration.read, declaration.write);
|
|
1585
|
+
if (presenceSettingsOf(declaration.read) !== null) this.#presenceDeclared = true;
|
|
1586
|
+
if (!isPattern) {
|
|
1587
|
+
const exact = new Map(this.#policyDeclarations ?? []);
|
|
1588
|
+
exact.set(name, declaration);
|
|
1589
|
+
this.#commitPolicies(exact, this.#channelPolicyPatterns);
|
|
1165
1590
|
return;
|
|
1166
1591
|
}
|
|
1167
|
-
|
|
1592
|
+
const re = compileChannelPattern(name);
|
|
1593
|
+
if (re === null) {
|
|
1594
|
+
this.#notifyOnce(`pattern-too-broad:${name}`, `le motif de politique "${name}" porte ${countWildcards(name)} caractères « * » (maximum ${RealtimeHub.MAX_PATTERN_WILDCARDS}) : il est IGNORÉ. Un motif de canal en demande un, deux au pire — au-delà, la mise en correspondance coûte cher sur des noms que le client choisit.`);
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const weight = channelPatternWeight(name);
|
|
1598
|
+
const entry = {
|
|
1599
|
+
...declaration,
|
|
1600
|
+
source: name,
|
|
1601
|
+
re,
|
|
1602
|
+
weight
|
|
1603
|
+
};
|
|
1604
|
+
const patterns = [...this.#channelPolicyPatterns ?? []];
|
|
1605
|
+
const at = patterns.findIndex((p) => p.source === name);
|
|
1606
|
+
if (at >= 0) patterns[at] = entry;
|
|
1607
|
+
else {
|
|
1608
|
+
const rank = patterns.findIndex((p) => p.weight < weight);
|
|
1609
|
+
if (rank < 0) patterns.push(entry);
|
|
1610
|
+
else patterns.splice(rank, 0, entry);
|
|
1611
|
+
}
|
|
1612
|
+
this.#commitPolicies(this.#policyDeclarations, patterns);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Valide l'ensemble des déclarations, recalcule la vue fusionnée de chaque nom exact, puis la publie — rien n'est publié si la validation refuse.
|
|
1616
|
+
*
|
|
1617
|
+
* La vue est recalculée à chaque inscription (cold path) pour qu'une trame
|
|
1618
|
+
* sur un nom exact reste UNE lecture de `Map`, motifs compris.
|
|
1619
|
+
*/
|
|
1620
|
+
#commitPolicies(exact, patterns) {
|
|
1621
|
+
const lock = this.#frameAuthorizer;
|
|
1622
|
+
const defect = policyDeclarationsDefect(exact, patterns, lock === null || lock.judgesWrite === true);
|
|
1623
|
+
if (defect !== null) throw new BootConfigurationError(defect);
|
|
1624
|
+
let merged = null;
|
|
1625
|
+
if (exact !== null) for (const [name, declaration] of exact) {
|
|
1626
|
+
const resolved = resolveDeclaredPolicies(name, declaration, patterns);
|
|
1627
|
+
if (resolved !== null) (merged ??= /* @__PURE__ */ new Map()).set(name, resolved);
|
|
1628
|
+
}
|
|
1629
|
+
this.#policyDeclarations = exact;
|
|
1630
|
+
this.#channelPolicyPatterns = patterns;
|
|
1631
|
+
this.#channelPolicies = merged;
|
|
1632
|
+
this.#dropRetainedVerdicts();
|
|
1168
1633
|
}
|
|
1169
1634
|
/**
|
|
1170
1635
|
* Nombre maximum de `*` dans un motif.
|
|
@@ -1178,37 +1643,6 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1178
1643
|
*/
|
|
1179
1644
|
static MAX_PATTERN_WILDCARDS = MAX_CHANNEL_PATTERN_WILDCARDS;
|
|
1180
1645
|
/**
|
|
1181
|
-
* Compile un motif et l'insère à son rang de spécificité.
|
|
1182
|
-
*
|
|
1183
|
-
* Le motif est ÉCHAPPÉ intégralement avant que `*` ne devienne `.*` : rien de
|
|
1184
|
-
* ce que l'auteur écrit n'est interprété comme une expression régulière. Un
|
|
1185
|
-
* `.` reste un point, un `(` reste une parenthèse — sans quoi une déclaration
|
|
1186
|
-
* anodine deviendrait une expression, avec ce que cela ouvre.
|
|
1187
|
-
*/
|
|
1188
|
-
#registerChannelPolicyPattern(source, entry) {
|
|
1189
|
-
const re = compileChannelPattern(source);
|
|
1190
|
-
if (re === null) {
|
|
1191
|
-
this.#notifyOnce(`pattern-too-broad:${source}`, `le motif de politique "${source}" porte ${countWildcards(source)} caractères « * » (maximum ${RealtimeHub.MAX_PATTERN_WILDCARDS}) : il est IGNORÉ. Un motif de canal en demande un, deux au pire — au-delà, la mise en correspondance coûte cher sur des noms que le client choisit.`);
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
const weight = source.split("*").join("").length;
|
|
1195
|
-
const policies = this.#channelPolicyPatterns ??= [];
|
|
1196
|
-
const existing = policies.findIndex((p) => p.source === source);
|
|
1197
|
-
const policyEntry = {
|
|
1198
|
-
source,
|
|
1199
|
-
re,
|
|
1200
|
-
weight,
|
|
1201
|
-
...entry
|
|
1202
|
-
};
|
|
1203
|
-
if (existing >= 0) {
|
|
1204
|
-
policies[existing] = policyEntry;
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
const rank = policies.findIndex((p) => p.weight < weight);
|
|
1208
|
-
if (rank < 0) policies.push(policyEntry);
|
|
1209
|
-
else policies.splice(rank, 0, policyEntry);
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
1646
|
* Politique déclarée pour ce canal/méthode, ou `null` si aucune. Lu par
|
|
1213
1647
|
* `@nodefony/security` (`resolveChannelPolicy` de la surface DI) au moment de
|
|
1214
1648
|
* la frame `subscribe`/inbound. O(1), 0 alloc quand le registre est vide.
|
|
@@ -1217,6 +1651,39 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1217
1651
|
return this.#resolvePolicyEntry(name)?.policy ?? null;
|
|
1218
1652
|
}
|
|
1219
1653
|
/**
|
|
1654
|
+
* Lecture ET écriture qui s'appliquent à ce nom, en UNE résolution — `null` si aucune politique ne le couvre (ADR-0022 §6, décision 1 quater).
|
|
1655
|
+
*
|
|
1656
|
+
* Même résolution que {@link resolveChannelPolicy} : l'entrée la plus
|
|
1657
|
+
* spécifique (nom exact, puis motif) gouverne les deux sortes. Rendue telle
|
|
1658
|
+
* quelle : aucune allocation, une seule recherche par trame jugée.
|
|
1659
|
+
*
|
|
1660
|
+
* @param name - nom du canal, de la méthode entrante ou de l'action.
|
|
1661
|
+
*/
|
|
1662
|
+
resolveChannelPolicies(name) {
|
|
1663
|
+
return this.#resolvePolicyEntry(name);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Retient la PREMIÈRE classe qui déclare une sorte de politique sous ce nom, et rend celle qui l'avait déjà déclarée — pour refuser au démarrage deux classes en désaccord.
|
|
1667
|
+
*
|
|
1668
|
+
* @param name - nom exact ou motif.
|
|
1669
|
+
* @param kind - sorte déclarée : lecture ou écriture.
|
|
1670
|
+
* @param owner - constructeur du contrôleur qui déclare.
|
|
1671
|
+
* @param policy - la politique déclarée.
|
|
1672
|
+
* @returns la déclaration antérieure de cette sorte, ou `null`.
|
|
1673
|
+
*/
|
|
1674
|
+
claimChannelPolicy(name, kind, owner, policy) {
|
|
1675
|
+
const claims = this.#policyClaims ??= /* @__PURE__ */ new Map();
|
|
1676
|
+
const key = `${kind}\u0000${name}`;
|
|
1677
|
+
const previous = claims.get(key);
|
|
1678
|
+
if (previous !== void 0) return previous;
|
|
1679
|
+
claims.set(key, {
|
|
1680
|
+
owner,
|
|
1681
|
+
ownerName: typeof owner === "function" ? owner.name : "(contrôleur)",
|
|
1682
|
+
policy
|
|
1683
|
+
});
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1220
1687
|
* Règle d'admission prête à appeler pour ce canal — `null` s'il ne déclare pas `allow`. Lue au `subscribe`, jamais par trame.
|
|
1221
1688
|
*
|
|
1222
1689
|
* Même résolution que {@link resolveChannelPolicy} (nom exact, puis motif le
|
|
@@ -1241,23 +1708,23 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1241
1708
|
addPolicyOwner(owner) {
|
|
1242
1709
|
(this.#policyOwners ??= /* @__PURE__ */ new WeakSet()).add(owner);
|
|
1243
1710
|
}
|
|
1244
|
-
/**
|
|
1711
|
+
/**
|
|
1712
|
+
* Politiques qui s'appliquent à ce nom : la vue fusionnée d'un nom exact (une lecture de `Map`), sinon la résolution par motifs, sorte par sorte.
|
|
1713
|
+
*/
|
|
1245
1714
|
#resolvePolicyEntry(name) {
|
|
1246
|
-
const
|
|
1247
|
-
if (
|
|
1715
|
+
const merged = this.#channelPolicies?.get(name);
|
|
1716
|
+
if (merged !== void 0) return merged;
|
|
1248
1717
|
const patterns = this.#channelPolicyPatterns;
|
|
1249
|
-
if (patterns
|
|
1250
|
-
|
|
1251
|
-
}
|
|
1252
|
-
return name.startsWith(USER_CHANNEL_PREFIX) ? USER_CHANNEL_POLICY : null;
|
|
1718
|
+
if (patterns === null) return name.startsWith(USER_CHANNEL_PREFIX) ? USER_CHANNEL_DECLARATION.self : null;
|
|
1719
|
+
return resolveDeclaredPolicies(name, void 0, patterns);
|
|
1253
1720
|
}
|
|
1254
1721
|
/**
|
|
1255
|
-
*
|
|
1722
|
+
* Ce nom EXACT porte-t-il une déclaration — motifs ignorés ?
|
|
1256
1723
|
*
|
|
1257
1724
|
* @param name - nom d'action ou de canal.
|
|
1258
1725
|
*/
|
|
1259
1726
|
hasExactChannelPolicy(name) {
|
|
1260
|
-
return this.#
|
|
1727
|
+
return this.#policyDeclarations?.has(name) === true;
|
|
1261
1728
|
}
|
|
1262
1729
|
/** Des politiques de canal sont-elles déclarées (au moins une) ? */
|
|
1263
1730
|
hasChannelPolicies() {
|
|
@@ -1305,16 +1772,6 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1305
1772
|
(this.#systemChannelFactories ??= /* @__PURE__ */ new Map()).set(channel, factory);
|
|
1306
1773
|
}
|
|
1307
1774
|
/**
|
|
1308
|
-
* Pont hot-path entre `JsonRpcPeer.beforeDispatch` et le verrou métier : lit
|
|
1309
|
-
* le token déjà caché du peer (O(1), 0 lecture base) puis délègue à
|
|
1310
|
-
* l'authorizer. `true` (autorisée) si aucun verrou posé (bypass 0-coût) —
|
|
1311
|
-
* mais en pratique `beforeDispatch` n'est branché que si `hasFrameAuthorizer()`.
|
|
1312
|
-
*
|
|
1313
|
-
* @param frame - frame JSON-RPC entrante (brute) à autoriser.
|
|
1314
|
-
* @param peer - peer émetteur (clé du mapping `peer → token`).
|
|
1315
|
-
* @returns `true` si la frame peut être dispatchée, `false` si refusée.
|
|
1316
|
-
*/
|
|
1317
|
-
/**
|
|
1318
1775
|
* Ouvre (ou referme) la réception des journaux du navigateur, avec ses bornes.
|
|
1319
1776
|
*
|
|
1320
1777
|
* @param limits - bornes de débit et de taille, ou `null` pour refermer le canal.
|
|
@@ -1326,6 +1783,38 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1326
1783
|
get clientLogsLimits() {
|
|
1327
1784
|
return this.#clientLogs;
|
|
1328
1785
|
}
|
|
1786
|
+
/**
|
|
1787
|
+
* La raison d'un refus du verrou, quand il sait la dire — `null` sinon, ou si l'explication lève.
|
|
1788
|
+
*
|
|
1789
|
+
* Chemin FROID : appelé après un refus, jamais par trame acceptée. Le
|
|
1790
|
+
* verdict, lui, est déjà rendu : une explication fautive ne change rien à la
|
|
1791
|
+
* décision, elle se tait.
|
|
1792
|
+
*
|
|
1793
|
+
* @param frame - la trame refusée.
|
|
1794
|
+
* @param peer - la connexion qui l'a émise.
|
|
1795
|
+
* @returns la phrase, ou `null`.
|
|
1796
|
+
*/
|
|
1797
|
+
explainDenial(frame, peer) {
|
|
1798
|
+
const explain = this.#frameAuthorizer?.explain;
|
|
1799
|
+
if (explain === void 0) return null;
|
|
1800
|
+
try {
|
|
1801
|
+
const said = explain(frame, this.getTokenForPeer(peer));
|
|
1802
|
+
return typeof said === "string" && said.length > 0 ? said : null;
|
|
1803
|
+
} catch {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Pont hot-path entre `JsonRpcPeer.beforeDispatch` et le verrou métier : lit
|
|
1809
|
+
* le token déjà caché du peer (O(1), 0 lecture base) puis délègue à
|
|
1810
|
+
* l'authorizer. `true` (autorisée) si aucun verrou posé : `beforeDispatch`
|
|
1811
|
+
* est armé sur TOUTE connexion, et c'est ici que le bypass se paie — un
|
|
1812
|
+
* appel et une comparaison de champ.
|
|
1813
|
+
*
|
|
1814
|
+
* @param frame - frame JSON-RPC entrante (brute) à autoriser.
|
|
1815
|
+
* @param peer - peer émetteur (clé du mapping `peer → token`).
|
|
1816
|
+
* @returns `true` si la frame peut être dispatchée, `false` si refusée.
|
|
1817
|
+
*/
|
|
1329
1818
|
runAuthorizer(frame, peer) {
|
|
1330
1819
|
const authorizer = this.#frameAuthorizer;
|
|
1331
1820
|
if (authorizer === null) return true;
|
|
@@ -1394,6 +1883,18 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1394
1883
|
}
|
|
1395
1884
|
/** Dispose tous les providers et vide le hub (arrêt process / reset de test). */
|
|
1396
1885
|
clear() {
|
|
1886
|
+
if (this.#presenceTimer !== null) clearTimeout(this.#presenceTimer);
|
|
1887
|
+
this.#presenceTimer = null;
|
|
1888
|
+
this.#presenceTimerAt = Number.POSITIVE_INFINITY;
|
|
1889
|
+
this.#presenceActive = null;
|
|
1890
|
+
this.#presenceBus = null;
|
|
1891
|
+
this.#presenceServed = null;
|
|
1892
|
+
this.#presenceLingering = null;
|
|
1893
|
+
this.#presenceDeclared = false;
|
|
1894
|
+
this.#presenceRejectedTotal = 0;
|
|
1895
|
+
this.#presenceListTotal = 0;
|
|
1896
|
+
this.#presenceFrameTotal = 0;
|
|
1897
|
+
this.#presenceListBuiltTotal = 0;
|
|
1397
1898
|
if (this.#channels) {
|
|
1398
1899
|
for (const st of this.#channels.values()) try {
|
|
1399
1900
|
st.dispose?.();
|
|
@@ -1413,6 +1914,8 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1413
1914
|
this.#channelPolicies = null;
|
|
1414
1915
|
this.#channelPolicyPatterns = null;
|
|
1415
1916
|
this.#policyOwners = null;
|
|
1917
|
+
this.#policyClaims = null;
|
|
1918
|
+
this.#policyDeclarations = null;
|
|
1416
1919
|
this.#originGuard = null;
|
|
1417
1920
|
this.#backplane = null;
|
|
1418
1921
|
this.#busBytes = false;
|
|
@@ -1425,6 +1928,7 @@ var RealtimeHub = class RealtimeHub {
|
|
|
1425
1928
|
this.#closedMalformedDiscarded = 0;
|
|
1426
1929
|
this.#inboundTotal = 0;
|
|
1427
1930
|
this.#ingressRejectedTotal = 0;
|
|
1931
|
+
this.#ingressUnknownDiscardedTotal = 0;
|
|
1428
1932
|
this.#localOnlyTotal = 0;
|
|
1429
1933
|
this.#systemFloorDeniedTotal = 0;
|
|
1430
1934
|
this.admissionBudget.reset();
|