@nodefony/realtime 10.0.0-beta.5 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +91 -12
- package/dist/nodefony/config/config.js +7 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +341 -43
- package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
- package/dist/nodefony/src/backplane/RedisBackplane.js +146 -41
- package/dist/nodefony/src/backplane/controlOrder.js +86 -0
- package/dist/nodefony/src/backplane/envelope.js +505 -27
- 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/cluster/ClusterProbeClient.js +2 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
- package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
- package/dist/nodefony/src/server/RealtimeConnection.js +135 -0
- package/dist/nodefony/src/server/RealtimeController.js +409 -107
- package/dist/nodefony/src/server/RealtimeHub.js +1158 -111
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
- package/dist/nodefony/src/server/admissionBudget.js +176 -0
- package/dist/nodefony/src/server/channelPolicyRegistry.js +95 -0
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/connectionAdmission.js +242 -0
- package/dist/nodefony/src/server/contractRoutes.js +75 -0
- package/dist/nodefony/src/server/deniedDetail.js +10 -3
- package/dist/nodefony/src/server/frameAdmission.js +160 -0
- 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 +13 -1
- package/dist/nodefony/src/server/syslogUplink.js +3 -4
- package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
- package/dist/nodefony/src/service/RealtimeService.js +51 -1
- package/dist/nodefony/src/service/publicOrigin.js +40 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
- package/dist/nodefony/testing/backplaneConformance.js +347 -0
- package/dist/nodefony/testing/index.js +43 -12
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +39 -7
- package/dist/types/nodefony/config/config.d.ts +9 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +198 -17
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +46 -51
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
- package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +89 -18
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +75 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +77 -0
- 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/AnonymousRealtimeToken.d.ts +10 -0
- package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +20 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +110 -12
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +448 -42
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
- package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +41 -0
- 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/connectionAdmission.d.ts +68 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +66 -0
- 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 +10 -0
- package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +17 -2
- package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +13 -2
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +420 -18
- package/docs/architecture.md +101 -11
- package/docs/cadence-et-contre-pression.md +87 -11
- package/docs/canaux-a-membres.md +277 -0
- package/docs/cas-d-usage.md +1703 -0
- package/docs/configuration.md +175 -19
- package/docs/cookbook-chat.md +701 -224
- package/docs/index.md +11 -1
- package/docs/observabilite.md +97 -21
- package/docs/protocole.md +142 -33
- package/docs/securite.md +347 -41
- package/docs/vocabulaire.md +13 -12
- package/package.json +16 -13
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
2
|
+
// nodefony/src/server/channelPresence.ts
|
|
3
|
+
/**
|
|
4
|
+
* Fenêtre de regroupement des changements de présence vers le navigateur (ms) — front montant : le premier changement d'un canal calme part tout de suite, les suivants attendent la fin de la fenêtre (ADR-0025 P2 bis).
|
|
5
|
+
*/
|
|
6
|
+
const PRESENCE_WINDOW_MS = 250;
|
|
7
|
+
/** Seuil par défaut d'identités nommées au-delà duquel la trame ne porte que les nombres (ADR-0025 P2 bis). */
|
|
8
|
+
const DEFAULT_PRESENCE_MAX_NAMED = 1e3;
|
|
9
|
+
const PRESENCE_OPTION_KEYS = /* @__PURE__ */ new Set(["grace", "maxNamed"]);
|
|
10
|
+
/**
|
|
11
|
+
* Départ d'un PAIR retenu par la grâce (pod oublié, arrêté, remplacé) — ses onglets restent comptés jusqu'à l'échéance. Forme fixe : alloué au seul départ d'un pod, chemin froid.
|
|
12
|
+
*/
|
|
13
|
+
var RemoteDeparture = class {
|
|
14
|
+
deadline;
|
|
15
|
+
tabs;
|
|
16
|
+
constructor(deadline, tabs) {
|
|
17
|
+
this.deadline = deadline;
|
|
18
|
+
this.tabs = tabs;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/** Entier sûr ≥ 0 ? */
|
|
22
|
+
function isCount(value) {
|
|
23
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Défaut de la valeur de la clé `presence` d'une politique, ou `null` si elle est recevable — en clair, pour que le refus au démarrage dise QUOI corriger.
|
|
27
|
+
*
|
|
28
|
+
* @param value - valeur de `policy.presence` (`unknown` : une politique se lit
|
|
29
|
+
* depuis une métadonnée publique).
|
|
30
|
+
*/
|
|
31
|
+
function presenceDefect(value) {
|
|
32
|
+
if (value === void 0 || typeof value === "boolean") return null;
|
|
33
|
+
if (!isRecord(value) || Array.isArray(value)) return "« presence » doit être un booléen ou un objet d'options";
|
|
34
|
+
for (const key of Object.keys(value)) if (!PRESENCE_OPTION_KEYS.has(key)) return `« presence » : option inconnue « ${key} » (options admises : ${[...PRESENCE_OPTION_KEYS].join(", ")})`;
|
|
35
|
+
if (value.grace !== void 0 && !isCount(value.grace)) return "« presence.grace » doit être un entier de millisecondes ≥ 0";
|
|
36
|
+
if (value.maxNamed !== void 0 && !isCount(value.maxNamed)) return "« presence.maxNamed » doit être un entier ≥ 0";
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Réglages résolus de la présence qu'une politique déclare, ou `null` si elle n'en déclare pas.
|
|
41
|
+
*
|
|
42
|
+
* Précondition : la politique a passé {@link presenceDefect}.
|
|
43
|
+
*/
|
|
44
|
+
function presenceSettingsOf(policy) {
|
|
45
|
+
const presence = policy?.presence;
|
|
46
|
+
if (presence === void 0 || presence === false) return null;
|
|
47
|
+
if (presence === true) return {
|
|
48
|
+
grace: 0,
|
|
49
|
+
maxNamed: DEFAULT_PRESENCE_MAX_NAMED
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
grace: presence.grace ?? 0,
|
|
53
|
+
maxNamed: presence.maxNamed ?? 1e3
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deux politiques déclarent-elles la même présence ? Défauts appliqués : `true` et `{ grace: 0 }` sont la même.
|
|
58
|
+
*
|
|
59
|
+
* Sert à refuser deux déclarations d'un même nom qui ne diffèrent que par la
|
|
60
|
+
* présence — une seule s'appliquerait. Démarrage seulement.
|
|
61
|
+
*/
|
|
62
|
+
function samePresence(a, b) {
|
|
63
|
+
const x = presenceSettingsOf(a);
|
|
64
|
+
const y = presenceSettingsOf(b);
|
|
65
|
+
if (x === null || y === null) return x === y;
|
|
66
|
+
return x.grace === y.grace && x.maxNamed === y.maxNamed;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* État de présence LOCAL d'un canal à présence (ADR-0025 P2 bis, P5) — identités nommées et onglets, connexions non nommées, version, changements en attente de la fenêtre, départs en grâce.
|
|
70
|
+
*
|
|
71
|
+
* Alloué au premier client d'un canal qui déclare `presence`, jamais pour un
|
|
72
|
+
* autre canal. Ne connaît ni l'horloge ni le protocole : le hub lui passe
|
|
73
|
+
* l'instant et fabrique les trames. Les changements portent des valeurs
|
|
74
|
+
* ABSOLUES (nombre d'onglets, départ), donc rejouer un changement déjà inclus
|
|
75
|
+
* dans une liste ne change rien — c'est ce qui permet de servir à un nouvel
|
|
76
|
+
* abonné la liste prise au dernier envoi, encodée une fois.
|
|
77
|
+
*
|
|
78
|
+
* Sur un canal DIFFUSÉ entre pods, c'est aussi l'agrégat de la vue des pairs
|
|
79
|
+
* ({@link IPresenceAggregate}) : les compteurs portent le TOTAL (local + pairs),
|
|
80
|
+
* et une table à part garde la seule part locale — ce que ce pod annonce à ses
|
|
81
|
+
* pairs, en deltas par lot. Ailleurs, cette table n'existe pas.
|
|
82
|
+
*/
|
|
83
|
+
var ChannelPresence = class {
|
|
84
|
+
channel;
|
|
85
|
+
settings;
|
|
86
|
+
/** Canal diffusé entre pods : sa part locale part sur le bus, la vue des pairs s'y ajoute. */
|
|
87
|
+
distributed;
|
|
88
|
+
/** Fin de la fenêtre de regroupement en cours (horloge monotone, ms). */
|
|
89
|
+
windowEnd = Number.NEGATIVE_INFINITY;
|
|
90
|
+
/** Liste complète encodée, servie telle quelle jusqu'au prochain envoi. */
|
|
91
|
+
listFrame = null;
|
|
92
|
+
/** Le canal est-il encore tenu par le hub ? `false` après son retrait. */
|
|
93
|
+
live = true;
|
|
94
|
+
/**
|
|
95
|
+
* Fin de la synchronisation initiale avec les pairs (ms monotone) — la vue
|
|
96
|
+
* n'est que locale tant que la demande d'état du premier abonné n'a pas eu le
|
|
97
|
+
* temps d'être servie ; `0` = synchronisée (ou canal non diffusé).
|
|
98
|
+
*/
|
|
99
|
+
syncUntil = 0;
|
|
100
|
+
#named = /* @__PURE__ */ new Map();
|
|
101
|
+
#unnamed = 0;
|
|
102
|
+
#local;
|
|
103
|
+
#localUnnamed = 0;
|
|
104
|
+
#busBefore = null;
|
|
105
|
+
#busUnnamedBefore = -1;
|
|
106
|
+
#partial = false;
|
|
107
|
+
#version = 0;
|
|
108
|
+
#dirty = null;
|
|
109
|
+
#countsDirty = false;
|
|
110
|
+
#namedSent = true;
|
|
111
|
+
#resumeNamed;
|
|
112
|
+
#leaving = null;
|
|
113
|
+
#remoteLeaving = null;
|
|
114
|
+
/**
|
|
115
|
+
* @param distributed - le canal est diffusé entre pods (bus branché, préfixe
|
|
116
|
+
* diffusé) : la part locale est tenue à part pour le bus.
|
|
117
|
+
*/
|
|
118
|
+
constructor(channel, settings, distributed = false) {
|
|
119
|
+
this.channel = channel;
|
|
120
|
+
this.settings = settings;
|
|
121
|
+
this.distributed = distributed;
|
|
122
|
+
this.#local = distributed ? /* @__PURE__ */ new Map() : null;
|
|
123
|
+
this.#resumeNamed = settings.maxNamed - Math.floor(settings.maxNamed / 10);
|
|
124
|
+
}
|
|
125
|
+
/** La part locale a-t-elle changé depuis le dernier lot ? */
|
|
126
|
+
get busDirty() {
|
|
127
|
+
return this.#busBefore !== null || this.#busUnnamedBefore >= 0;
|
|
128
|
+
}
|
|
129
|
+
/** La vue est-elle connue incomplète (synchronisation initiale, trou chez un pair) ? */
|
|
130
|
+
get partial() {
|
|
131
|
+
return this.#partial;
|
|
132
|
+
}
|
|
133
|
+
/** Version courante de la vue (+1 par trame de changements). */
|
|
134
|
+
get version() {
|
|
135
|
+
return this.#version;
|
|
136
|
+
}
|
|
137
|
+
/** Des changements attendent-ils un envoi ? */
|
|
138
|
+
get dirty() {
|
|
139
|
+
return this.#dirty !== null || this.#countsDirty;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Une connexion entre.
|
|
143
|
+
*
|
|
144
|
+
* @param key - identité nommée, ou `null` pour une connexion non nommée.
|
|
145
|
+
*/
|
|
146
|
+
join(key) {
|
|
147
|
+
const local = this.#local;
|
|
148
|
+
if (key === null) {
|
|
149
|
+
this.#unnamed += 1;
|
|
150
|
+
this.#countsDirty = true;
|
|
151
|
+
if (local !== null) this.#addLocalUnnamed(1);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (this.#leaving?.delete(key) === true) return;
|
|
155
|
+
const named = this.#named;
|
|
156
|
+
const before = named.get(key) ?? 0;
|
|
157
|
+
const total = before - this.#cancelRemoteDeparture(key) + 1;
|
|
158
|
+
named.set(key, total);
|
|
159
|
+
if (total !== before) this.#markDirty(key);
|
|
160
|
+
if (local !== null) this.#setLocal(local, key, (local.get(key) ?? 0) + 1);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Une connexion sort.
|
|
164
|
+
*
|
|
165
|
+
* Un départ nommé attend la grâce du canal ; un départ non nommé est décompté
|
|
166
|
+
* tout de suite (rien ne reconnaîtrait son retour).
|
|
167
|
+
*
|
|
168
|
+
* @param key - même valeur qu'à {@link ChannelPresence.join}.
|
|
169
|
+
* @param now - instant monotone (ms), pour la grâce.
|
|
170
|
+
*/
|
|
171
|
+
leave(key, now) {
|
|
172
|
+
const local = this.#local;
|
|
173
|
+
if (key === null) {
|
|
174
|
+
this.#unnamed -= 1;
|
|
175
|
+
this.#countsDirty = true;
|
|
176
|
+
if (local !== null) this.#addLocalUnnamed(-1);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const grace = this.settings.grace;
|
|
180
|
+
const tabs = this.#named.get(key);
|
|
181
|
+
if (tabs === void 0) return;
|
|
182
|
+
if (tabs > 1) {
|
|
183
|
+
this.#named.set(key, tabs - 1);
|
|
184
|
+
this.#markDirty(key);
|
|
185
|
+
if (local !== null) {
|
|
186
|
+
const mine = local.get(key) ?? 0;
|
|
187
|
+
if (mine > 0) this.#setLocal(local, key, mine - 1);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (grace > 0) {
|
|
192
|
+
(this.#leaving ??= /* @__PURE__ */ new Map()).set(key, now + grace);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.#named.delete(key);
|
|
196
|
+
this.#markDirty(key);
|
|
197
|
+
if (local !== null) this.#setLocal(local, key, 0);
|
|
198
|
+
}
|
|
199
|
+
/** Publie les départs dont la grâce est écoulée. */
|
|
200
|
+
expire(now) {
|
|
201
|
+
const leaving = this.#leaving;
|
|
202
|
+
const local = this.#local;
|
|
203
|
+
if (this.#remoteLeaving !== null) this.#expireRemote(now);
|
|
204
|
+
if (leaving === null) return;
|
|
205
|
+
for (const [key, deadline] of leaving) {
|
|
206
|
+
if (deadline > now) break;
|
|
207
|
+
leaving.delete(key);
|
|
208
|
+
this.#named.delete(key);
|
|
209
|
+
this.#markDirty(key);
|
|
210
|
+
if (local !== null) this.#setLocal(local, key, 0);
|
|
211
|
+
}
|
|
212
|
+
if (leaving.size === 0) this.#leaving = null;
|
|
213
|
+
}
|
|
214
|
+
/** Prochaine échéance de grâce (ms monotone), `Infinity` sans départ retenu — O(1). */
|
|
215
|
+
nextDeadline() {
|
|
216
|
+
let next = Number.POSITIVE_INFINITY;
|
|
217
|
+
if (this.#leaving !== null) for (const deadline of this.#leaving.values()) {
|
|
218
|
+
next = deadline;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
if (this.#remoteLeaving !== null) for (const departure of this.#remoteLeaving.values()) {
|
|
222
|
+
if (departure.deadline < next) next = departure.deadline;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
return next;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Trame complète à la version COURANTE — la liste, ou les nombres seuls au-delà du seuil.
|
|
229
|
+
*
|
|
230
|
+
* Une liste ne prend jamais de numéro : servie à un nouvel abonné, elle ferait
|
|
231
|
+
* voir un trou à tous les autres (ADR-0025 P2 bis).
|
|
232
|
+
*/
|
|
233
|
+
listParams() {
|
|
234
|
+
const named = this.#named;
|
|
235
|
+
const count = named.size;
|
|
236
|
+
const channel = this.channel;
|
|
237
|
+
const version = this.#version;
|
|
238
|
+
const unnamed = this.#unnamed;
|
|
239
|
+
if (!this.#wantsNamed(count)) return this.#partial ? {
|
|
240
|
+
channel,
|
|
241
|
+
version,
|
|
242
|
+
count,
|
|
243
|
+
unnamed,
|
|
244
|
+
partial: true
|
|
245
|
+
} : {
|
|
246
|
+
channel,
|
|
247
|
+
version,
|
|
248
|
+
count,
|
|
249
|
+
unnamed
|
|
250
|
+
};
|
|
251
|
+
const members = pairsOf(named);
|
|
252
|
+
return this.#partial ? {
|
|
253
|
+
channel,
|
|
254
|
+
version,
|
|
255
|
+
count,
|
|
256
|
+
unnamed,
|
|
257
|
+
members,
|
|
258
|
+
partial: true
|
|
259
|
+
} : {
|
|
260
|
+
channel,
|
|
261
|
+
version,
|
|
262
|
+
count,
|
|
263
|
+
unnamed,
|
|
264
|
+
members
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Consomme les changements en attente et rend la trame à envoyer à tous les membres, ou `null` s'il n'y en a pas.
|
|
269
|
+
*
|
|
270
|
+
* Sous le seuil : les changements, version +1. Au-delà : les nombres seuls,
|
|
271
|
+
* version inchangée (trame complète). Au retour sous le seuil : la liste.
|
|
272
|
+
* Toute trame envoyée périme la liste encodée.
|
|
273
|
+
*/
|
|
274
|
+
takeChanges() {
|
|
275
|
+
if (!this.dirty) return null;
|
|
276
|
+
const dirty = this.#dirty;
|
|
277
|
+
this.#dirty = null;
|
|
278
|
+
this.#countsDirty = false;
|
|
279
|
+
this.listFrame = null;
|
|
280
|
+
const named = this.#wantsNamed(this.#named.size);
|
|
281
|
+
if (!named || !this.#namedSent) {
|
|
282
|
+
this.#namedSent = named;
|
|
283
|
+
return this.listParams();
|
|
284
|
+
}
|
|
285
|
+
this.#version += 1;
|
|
286
|
+
let set = null;
|
|
287
|
+
let gone = null;
|
|
288
|
+
if (dirty !== null) for (const key of dirty) {
|
|
289
|
+
const tabs = this.#named.get(key);
|
|
290
|
+
if (tabs === void 0) (gone ??= []).push(key);
|
|
291
|
+
else (set ??= []).push(key, tabs);
|
|
292
|
+
}
|
|
293
|
+
const changes = set === null ? gone === null ? {} : { gone } : gone === null ? { set } : {
|
|
294
|
+
set,
|
|
295
|
+
gone
|
|
296
|
+
};
|
|
297
|
+
const channel = this.channel;
|
|
298
|
+
const version = this.#version;
|
|
299
|
+
const count = this.#named.size;
|
|
300
|
+
const unnamed = this.#unnamed;
|
|
301
|
+
return this.#partial ? {
|
|
302
|
+
channel,
|
|
303
|
+
version,
|
|
304
|
+
count,
|
|
305
|
+
unnamed,
|
|
306
|
+
changes,
|
|
307
|
+
partial: true
|
|
308
|
+
} : {
|
|
309
|
+
channel,
|
|
310
|
+
version,
|
|
311
|
+
count,
|
|
312
|
+
unnamed,
|
|
313
|
+
changes
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/** Vue de l'observateur serveur : les membres toujours, une copie. */
|
|
317
|
+
view() {
|
|
318
|
+
const members = /* @__PURE__ */ new Map();
|
|
319
|
+
for (const [key, tabs] of this.#named) members.set(key, { tabs });
|
|
320
|
+
return {
|
|
321
|
+
count: this.#named.size,
|
|
322
|
+
unnamed: this.#unnamed,
|
|
323
|
+
members,
|
|
324
|
+
partial: this.#partial
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Pose l'état « incomplet » de la vue ; un changement fait partir une trame (la version suit) et périme la liste encodée.
|
|
329
|
+
*/
|
|
330
|
+
setPartial(partial) {
|
|
331
|
+
if (partial === this.#partial) return;
|
|
332
|
+
this.#partial = partial;
|
|
333
|
+
this.#countsDirty = true;
|
|
334
|
+
this.listFrame = null;
|
|
335
|
+
}
|
|
336
|
+
/** {@inheritDoc IPresenceAggregate.remoteTabs} */
|
|
337
|
+
remoteTabs(key, delta) {
|
|
338
|
+
const named = this.#named;
|
|
339
|
+
const before = named.get(key) ?? 0;
|
|
340
|
+
let total = before;
|
|
341
|
+
if (delta > 0) {
|
|
342
|
+
if (this.#leaving?.delete(key) === true) {
|
|
343
|
+
total -= 1;
|
|
344
|
+
const local = this.#local;
|
|
345
|
+
if (local !== null) this.#setLocal(local, key, 0);
|
|
346
|
+
} else total -= this.#cancelRemoteDeparture(key);
|
|
347
|
+
}
|
|
348
|
+
total += delta;
|
|
349
|
+
if (total > 0) named.set(key, total);
|
|
350
|
+
else named.delete(key);
|
|
351
|
+
if (total !== before) this.#markDirty(key);
|
|
352
|
+
}
|
|
353
|
+
/** {@inheritDoc IPresenceAggregate.remoteUnnamed} */
|
|
354
|
+
remoteUnnamed(delta) {
|
|
355
|
+
this.#unnamed += delta;
|
|
356
|
+
this.#countsDirty = true;
|
|
357
|
+
}
|
|
358
|
+
/** {@inheritDoc IPresenceAggregate.remoteLeave} */
|
|
359
|
+
remoteLeave(key, tabs, now) {
|
|
360
|
+
const grace = this.settings.grace;
|
|
361
|
+
if (grace === 0 || (this.#named.get(key) ?? 0) > tabs) {
|
|
362
|
+
this.remoteTabs(key, -tabs);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const leaving = this.#remoteLeaving ??= /* @__PURE__ */ new Map();
|
|
366
|
+
const pending = leaving.get(key);
|
|
367
|
+
if (pending !== void 0) pending.tabs += tabs;
|
|
368
|
+
else leaving.set(key, new RemoteDeparture(now + grace, tabs));
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Consomme les changements de la part locale depuis le dernier lot et rend l'entrée du lot, en valeurs absolues — `null` s'ils s'annulent tous.
|
|
372
|
+
*/
|
|
373
|
+
takeBusDelta() {
|
|
374
|
+
const local = this.#local;
|
|
375
|
+
const before = this.#busBefore;
|
|
376
|
+
const unnamedBefore = this.#busUnnamedBefore;
|
|
377
|
+
this.#busBefore = null;
|
|
378
|
+
this.#busUnnamedBefore = -1;
|
|
379
|
+
if (local === null) return null;
|
|
380
|
+
let set = null;
|
|
381
|
+
let gone = null;
|
|
382
|
+
if (before !== null) for (const [key, was] of before) {
|
|
383
|
+
const tabs = local.get(key) ?? 0;
|
|
384
|
+
if (tabs === was) continue;
|
|
385
|
+
if (tabs === 0) (gone ??= []).push(key);
|
|
386
|
+
else (set ??= []).push(key, tabs);
|
|
387
|
+
}
|
|
388
|
+
const unnamed = this.#localUnnamed;
|
|
389
|
+
if (set === null && gone === null && (unnamedBefore < 0 || unnamedBefore === unnamed)) return null;
|
|
390
|
+
return {
|
|
391
|
+
channel: this.channel,
|
|
392
|
+
set,
|
|
393
|
+
gone,
|
|
394
|
+
unnamed
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** État COMPLET de la part locale, pour une réponse d'état (paires à plat). */
|
|
398
|
+
localState() {
|
|
399
|
+
const local = this.#local;
|
|
400
|
+
return {
|
|
401
|
+
channel: this.channel,
|
|
402
|
+
members: local === null ? [] : pairsOf(local),
|
|
403
|
+
unnamed: this.#localUnnamed
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Des départs LOCAUX attendent-ils la fin de leur grâce ? Le dernier client
|
|
408
|
+
* parti d'un pod n'y échappe pas : la présence reste servie au bus jusqu'à
|
|
409
|
+
* l'échéance (ADR-0025 P5), puis se libère.
|
|
410
|
+
*/
|
|
411
|
+
get hasLocalGrace() {
|
|
412
|
+
return this.#leaving !== null;
|
|
413
|
+
}
|
|
414
|
+
/** La part locale compte-t-elle quelqu'un ? */
|
|
415
|
+
get hasLocal() {
|
|
416
|
+
return (this.#local?.size ?? 0) > 0 || this.#localUnnamed > 0;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Le canal n'est plus servi ici : toute la part locale part à zéro au prochain lot, pour que les pairs ne gardent aucun fantôme.
|
|
420
|
+
*/
|
|
421
|
+
release() {
|
|
422
|
+
const local = this.#local;
|
|
423
|
+
if (local === null) return;
|
|
424
|
+
for (const key of local.keys()) this.#setLocal(local, key, 0);
|
|
425
|
+
this.#leaving = null;
|
|
426
|
+
if (this.#localUnnamed !== 0) this.#addLocalUnnamed(-this.#localUnnamed);
|
|
427
|
+
this.#remoteLeaving = null;
|
|
428
|
+
}
|
|
429
|
+
/** Annule le départ en grâce d'un pair pour `key` ; rend les onglets qu'il retenait (`0` sans départ). */
|
|
430
|
+
#cancelRemoteDeparture(key) {
|
|
431
|
+
const leaving = this.#remoteLeaving;
|
|
432
|
+
if (leaving === null) return 0;
|
|
433
|
+
const departure = leaving.get(key);
|
|
434
|
+
if (departure === void 0) return 0;
|
|
435
|
+
leaving.delete(key);
|
|
436
|
+
if (leaving.size === 0) this.#remoteLeaving = null;
|
|
437
|
+
return departure.tabs;
|
|
438
|
+
}
|
|
439
|
+
/** Départs de pairs dont la grâce est écoulée — échéances croissantes, arrêt à la première non échue. */
|
|
440
|
+
#expireRemote(now) {
|
|
441
|
+
const leaving = this.#remoteLeaving;
|
|
442
|
+
if (leaving === null) return;
|
|
443
|
+
const named = this.#named;
|
|
444
|
+
for (const [key, departure] of leaving) {
|
|
445
|
+
if (departure.deadline > now) break;
|
|
446
|
+
leaving.delete(key);
|
|
447
|
+
const total = (named.get(key) ?? 0) - departure.tabs;
|
|
448
|
+
if (total > 0) named.set(key, total);
|
|
449
|
+
else named.delete(key);
|
|
450
|
+
this.#markDirty(key);
|
|
451
|
+
}
|
|
452
|
+
if (leaving.size === 0) this.#remoteLeaving = null;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* La trame complète doit-elle nommer `count` présents ? Nommée : tant qu'on ne
|
|
456
|
+
* dépasse pas `maxNamed`. En nombres seuls : seulement revenu au seuil de retour.
|
|
457
|
+
*/
|
|
458
|
+
#wantsNamed(count) {
|
|
459
|
+
return count <= (this.#namedSent ? this.settings.maxNamed : this.#resumeNamed);
|
|
460
|
+
}
|
|
461
|
+
#markDirty(key) {
|
|
462
|
+
(this.#dirty ??= /* @__PURE__ */ new Set()).add(key);
|
|
463
|
+
}
|
|
464
|
+
/** Pose les onglets LOCAUX d'une identité, en retenant la valeur du dernier lot. */
|
|
465
|
+
#setLocal(local, key, tabs) {
|
|
466
|
+
const before = local.get(key) ?? 0;
|
|
467
|
+
if (tabs === before) return;
|
|
468
|
+
const pending = this.#busBefore ??= /* @__PURE__ */ new Map();
|
|
469
|
+
if (!pending.has(key)) pending.set(key, before);
|
|
470
|
+
if (tabs === 0) local.delete(key);
|
|
471
|
+
else local.set(key, tabs);
|
|
472
|
+
}
|
|
473
|
+
#addLocalUnnamed(delta) {
|
|
474
|
+
if (this.#busUnnamedBefore < 0) this.#busUnnamedBefore = this.#localUnnamed;
|
|
475
|
+
this.#localUnnamed += delta;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
/** Paires à plat `[identité, onglets, …]` d'une table — tableau pré-dimensionné, rempli en place. */
|
|
479
|
+
function pairsOf(table) {
|
|
480
|
+
const pairs = new Array(table.size * 2);
|
|
481
|
+
let i = 0;
|
|
482
|
+
for (const [key, tabs] of table) {
|
|
483
|
+
pairs[i++] = key;
|
|
484
|
+
pairs[i++] = tabs;
|
|
485
|
+
}
|
|
486
|
+
return pairs;
|
|
487
|
+
}
|
|
488
|
+
export { ChannelPresence, DEFAULT_PRESENCE_MAX_NAMED, PRESENCE_WINDOW_MS, presenceDefect, presenceSettingsOf, samePresence };
|