@aurabase/realtime 0.4.1 → 0.5.0
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 +91 -4
- package/dist/index.cjs +302 -119
- package/dist/index.d.cts +182 -94
- package/dist/index.d.ts +182 -94
- package/dist/index.js +303 -120
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChannelConfig, PostgresChangesFilter, RealtimeEvent, BroadcastFilter, PresenceFilter, SubscribeCallback, BroadcastPayload,
|
|
1
|
+
import { ChannelConfig, PostgresChangesFilter, RealtimeEvent, BroadcastFilter, PresenceFilter, PresenceState, SubscribeCallback, BroadcastPayload, BroadcastSendStatus, ServerMessage, RealtimeListenType, AuraClientLike, AuraResponse } from '@aurabase/core';
|
|
2
2
|
|
|
3
3
|
type ListenerCallback = (payload: RealtimeEvent | Record<string, unknown>) => void;
|
|
4
4
|
type ListenerEntry = {
|
|
@@ -6,7 +6,15 @@ type ListenerEntry = {
|
|
|
6
6
|
filter: PostgresChangesFilter | BroadcastFilter | PresenceFilter;
|
|
7
7
|
callback: ListenerCallback;
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Fonction d'émission fournie par `RealtimeService`.
|
|
11
|
+
*
|
|
12
|
+
* Retourne `false` lorsque le message n'a **pas** été remis à la socket (socket absente
|
|
13
|
+
* ou non-OPEN — `RealtimeService._send` jette silencieusement ce qu'il ne peut pas
|
|
14
|
+
* émettre). Tout autre retour (`true`, `undefined`…) vaut « remis au transport » : les
|
|
15
|
+
* transports de test qui ne retournent rien restent valides.
|
|
16
|
+
*/
|
|
17
|
+
type SendFn = (data: string) => boolean | void;
|
|
10
18
|
/**
|
|
11
19
|
* Délai par défaut d'attente de la confirmation `subscribed` avant d'émettre
|
|
12
20
|
* `TIMED_OUT`. Aligné sur `DEFAULT_TIMEOUT` de `@supabase/realtime-js`
|
|
@@ -35,7 +43,7 @@ declare const DEFAULT_SUBSCRIBE_TIMEOUT_MS = 10000;
|
|
|
35
43
|
*/
|
|
36
44
|
declare class AuraChannel {
|
|
37
45
|
readonly name: string;
|
|
38
|
-
private readonly
|
|
46
|
+
private readonly _projectPrefix;
|
|
39
47
|
private readonly _listeners;
|
|
40
48
|
private _subscribed;
|
|
41
49
|
private _sendFn;
|
|
@@ -51,8 +59,18 @@ declare class AuraChannel {
|
|
|
51
59
|
private _refCounter;
|
|
52
60
|
/** Dernier event ID recu — utilise pour since_id au reconnect */
|
|
53
61
|
private _lastEventId;
|
|
54
|
-
/** Pending ACK callbacks : ref_id →
|
|
62
|
+
/** Pending ACK callbacks : ref_id → solde la promesse de `send()` avec un statut. */
|
|
55
63
|
private _pendingAcks;
|
|
64
|
+
/**
|
|
65
|
+
* Abonnement vu par le SERVEUR — à ne pas confondre avec `_subscribed`, qui n'est que
|
|
66
|
+
* l'INTENTION de l'appelant (et pilote le re-subscribe après reconnexion).
|
|
67
|
+
*
|
|
68
|
+
* C18 (rejeu) : `_subscribed` était posé optimistement par `subscribe()` et n'était
|
|
69
|
+
* jamais infirmé quand le serveur REFUSAIT l'abonnement. `send({ ack })` armait alors un
|
|
70
|
+
* compte à rebours de 5 s pour un accusé structurellement impossible — mesuré sur le banc
|
|
71
|
+
* à 5 065 ms, alors que le serveur avait répondu en 6 ms « Non abonné au canal … ».
|
|
72
|
+
*/
|
|
73
|
+
private _serverState;
|
|
56
74
|
/**
|
|
57
75
|
* Timer d'attente de la confirmation `subscribed`. Armé à l'ENVOI effectif du
|
|
58
76
|
* message subscribe (parité phoenix : `Push#startTimeout` ne démarre qu'au
|
|
@@ -63,7 +81,14 @@ declare class AuraChannel {
|
|
|
63
81
|
private _subscribeTimeoutMs;
|
|
64
82
|
/** `CLOSED` n'est émis qu'une fois par cycle de vie du canal (unsubscribe idempotent). */
|
|
65
83
|
private _closedEmitted;
|
|
66
|
-
|
|
84
|
+
/**
|
|
85
|
+
* @param projectPrefix Préfixe de projet du canal sur le fil (`"<project_id>:"`, ou `""`
|
|
86
|
+
* s'il est inconnu), résolu PARESSEUSEMENT : le SDK ne connaît le projet qu'à travers
|
|
87
|
+
* le token de session, qui peut arriver après la création du canal (connexion
|
|
88
|
+
* différée). Une valeur figée à la construction serait alors périmée pour toujours,
|
|
89
|
+
* le canal étant mémorisé par `RealtimeService`.
|
|
90
|
+
*/
|
|
91
|
+
constructor(name: string, projectPrefix: () => string, options?: ChannelConfig);
|
|
67
92
|
/** Génère un ref_id unique pour la corrélation request/response. */
|
|
68
93
|
private _nextRef;
|
|
69
94
|
/** Full channel name with project prefix (for server protocol) */
|
|
@@ -74,7 +99,7 @@ declare class AuraChannel {
|
|
|
74
99
|
*/
|
|
75
100
|
on(type: 'postgres_changes', filter: PostgresChangesFilter, callback: (payload: RealtimeEvent) => void): this;
|
|
76
101
|
on(type: 'broadcast', filter: BroadcastFilter, callback: (payload: RealtimeEvent) => void): this;
|
|
77
|
-
on(type: 'presence', filter: PresenceFilter, callback: (payload:
|
|
102
|
+
on(type: 'presence', filter: PresenceFilter, callback: (payload: PresenceState | RealtimeEvent | Record<string, unknown>) => void): this;
|
|
78
103
|
/**
|
|
79
104
|
* Subscribe to the channel. Sends the Subscribe message to the server
|
|
80
105
|
* with the full config (postgres_changes, broadcast, presence).
|
|
@@ -131,19 +156,36 @@ declare class AuraChannel {
|
|
|
131
156
|
/** Unsubscribe from the channel. */
|
|
132
157
|
unsubscribe(): void;
|
|
133
158
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
159
|
+
* Envoie un message broadcast sur le canal.
|
|
160
|
+
*
|
|
161
|
+
* Avec `broadcast: { ack: true }`, la promesse ne se résout qu'à l'accusé de réception du
|
|
162
|
+
* serveur (`broadcast_ack`) ; sans `ack`, elle se résout dès la remise au transport.
|
|
163
|
+
*
|
|
164
|
+
* Le statut rendu nomme sa CAUSE — voir {@link BroadcastSendStatus} :
|
|
165
|
+
* `'ok'`, `'timeout'`, `'not_connected'`, `'requires_subscription'`, `'error'`.
|
|
166
|
+
*
|
|
167
|
+
* **`'requires_subscription'`** signifie que le serveur n'a pas d'abonnement pour ce canal
|
|
168
|
+
* — soit `.subscribe()` n'a jamais été appelé, soit le serveur l'a REFUSÉ (RLS,
|
|
169
|
+
* `private_only`, quota) ou révoqué depuis. Rien n'est émis dans ce cas : le serveur
|
|
170
|
+
* rejetterait l'écriture (`Non abonné au canal '…' — Subscribe requis avant Broadcast`).
|
|
136
171
|
*
|
|
137
172
|
* @example
|
|
138
173
|
* ```ts
|
|
139
|
-
* //
|
|
174
|
+
* // Sans ACK (fire & forget)
|
|
140
175
|
* channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100, y: 200 } })
|
|
141
176
|
*
|
|
142
|
-
* //
|
|
143
|
-
*
|
|
177
|
+
* // Avec ACK — le modèle est celui de Supabase : on écrit APRÈS SUBSCRIBED.
|
|
178
|
+
* channel.subscribe(async (statut) => {
|
|
179
|
+
* if (statut !== 'SUBSCRIBED') return // CHANNEL_ERROR = le serveur a refusé ce canal
|
|
180
|
+
* const status = await channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100 } })
|
|
181
|
+
* if (status === 'requires_subscription') {
|
|
182
|
+
* // l'abonnement a été refusé ou révoqué entre-temps ; la cause a été remontée en
|
|
183
|
+
* // CHANNEL_ERROR à ce même callback.
|
|
184
|
+
* }
|
|
185
|
+
* })
|
|
144
186
|
* ```
|
|
145
187
|
*/
|
|
146
|
-
send(message: BroadcastPayload): Promise<
|
|
188
|
+
send(message: BroadcastPayload): Promise<BroadcastSendStatus>;
|
|
147
189
|
/**
|
|
148
190
|
* Track presence state for this client.
|
|
149
191
|
* Returns a Promise that resolves with 'ok' once tracked (compatible Supabase).
|
|
@@ -154,12 +196,20 @@ declare class AuraChannel {
|
|
|
154
196
|
* ```
|
|
155
197
|
*/
|
|
156
198
|
track(state: Record<string, unknown>): Promise<'ok' | 'error'>;
|
|
157
|
-
/** Stop tracking presence. */
|
|
199
|
+
/** Stop tracking presence. Même contrat que `track()`. */
|
|
158
200
|
untrack(): Promise<'ok' | 'error'>;
|
|
159
201
|
/** Get the current presence state for this channel. */
|
|
160
202
|
presenceState(): PresenceState;
|
|
161
203
|
/** Called by RealtimeService when a server message arrives for this channel. */
|
|
162
204
|
_onMessage(msg: ServerMessage): void;
|
|
205
|
+
/**
|
|
206
|
+
* Solde TOUS les accusés en attente de ce canal avec un statut donné.
|
|
207
|
+
*
|
|
208
|
+
* Le protocole ne corrèle pas l'erreur serveur au `ref_id` de l'écriture refusée : quand
|
|
209
|
+
* le serveur déclare n'avoir aucun abonnement pour ce canal, AUCUN accusé en vol ne peut
|
|
210
|
+
* plus aboutir — les solder tous est donc exact, pas approximatif.
|
|
211
|
+
*/
|
|
212
|
+
private _settlePendingAcks;
|
|
163
213
|
/** Dispatch a RealtimeEvent to matching listeners. */
|
|
164
214
|
private _dispatchEvent;
|
|
165
215
|
/** Whether this channel has been subscribed. */
|
|
@@ -209,6 +259,73 @@ declare class RealtimeConnectionError extends Error {
|
|
|
209
259
|
});
|
|
210
260
|
}
|
|
211
261
|
|
|
262
|
+
/**
|
|
263
|
+
* HeartbeatManager — pings périodiques pour maintenir la connexion WS.
|
|
264
|
+
*
|
|
265
|
+
* Utilise un Worker dédié : les navigateurs throttlent agressivement les timers
|
|
266
|
+
* d'un onglet en arrière-plan (jusqu'à 1 tick/minute), ce qui ferait manquer le
|
|
267
|
+
* `heartbeat_timeout` de 60 s du serveur ; un Worker n'est pas soumis à ce
|
|
268
|
+
* throttling. Fallback `setInterval` quand le Worker est indisponible.
|
|
269
|
+
*
|
|
270
|
+
* ── Pourquoi trois filets et pas un ──────────────────────────────────────────
|
|
271
|
+
* Ce battement ne sert plus seulement à tenir la connexion ouverte : c'est aussi
|
|
272
|
+
* lui qui cadence la **détection de connexion morte** du `RealtimeService`. S'il
|
|
273
|
+
* se tait, le client ne ping plus (le serveur coupe à 60 s) *et* ne détecte plus
|
|
274
|
+
* rien. Les trois modes de défaillance d'un Worker sont donc couverts :
|
|
275
|
+
*
|
|
276
|
+
* 1. **Échec synchrone** (`new Worker` lève, `Worker` absent) → `try/catch`.
|
|
277
|
+
* 2. **Échec asynchrone** — le cas d'une CSP `worker-src` qui n'autorise pas
|
|
278
|
+
* `blob:`. La spec HTML est explicite : quand le fetch du script échoue,
|
|
279
|
+
* le constructeur **ne lève pas**, l'agent utilisateur déclenche un événement
|
|
280
|
+
* `error` sur l'objet Worker. Un `try/catch` ne peut donc rien attraper →
|
|
281
|
+
* `onerror` bascule sur le fallback.
|
|
282
|
+
* 3. **Worker inerte** — construit sans erreur mais qui ne poste jamais rien.
|
|
283
|
+
* Aucun événement ne le signale : le Worker accuse donc réception du `start`
|
|
284
|
+
* par un `ready` immédiat, et un chien de garde bascule si cet accusé
|
|
285
|
+
* n'arrive pas.
|
|
286
|
+
*
|
|
287
|
+
* Le chien de garde porte sur le `ready`, PAS sur le premier battement : mesuré
|
|
288
|
+
* en Chromium réel, le démarrage d'un Worker peut dépasser 1,5 intervalle quand
|
|
289
|
+
* l'intervalle est court, ce qui faisait abandonner à tort un Worker
|
|
290
|
+
* parfaitement sain — et perdre silencieusement l'immunité au throttling, la
|
|
291
|
+
* seule raison d'être de ce Worker.
|
|
292
|
+
*/
|
|
293
|
+
declare class HeartbeatManager {
|
|
294
|
+
private _worker;
|
|
295
|
+
private _workerUrl;
|
|
296
|
+
private _interval;
|
|
297
|
+
private _sendFn;
|
|
298
|
+
/** Chien de garde de l'accusé `ready` du Worker (mode de défaillance 3). */
|
|
299
|
+
private _watchdog;
|
|
300
|
+
/**
|
|
301
|
+
* Numéro du cycle start/stop courant. Un événement asynchrone (erreur de
|
|
302
|
+
* Worker, chien de garde) portant un numéro périmé appartient à un cycle déjà
|
|
303
|
+
* arrêté ou remplacé : il doit être ignoré, jamais ressusciter un heartbeat.
|
|
304
|
+
*/
|
|
305
|
+
private _generation;
|
|
306
|
+
/**
|
|
307
|
+
* Délai accordé au Worker pour accuser réception du `start`.
|
|
308
|
+
*
|
|
309
|
+
* Volontairement **indépendant de l'intervalle de heartbeat** : c'est un temps
|
|
310
|
+
* de démarrage de thread, pas un multiple de la cadence. 3 s est très large
|
|
311
|
+
* pour un Worker sain (quelques millisecondes en pratique) tout en restant
|
|
312
|
+
* loin sous le `heartbeat_timeout` de 60 s du serveur.
|
|
313
|
+
*/
|
|
314
|
+
private static readonly WORKER_READY_TIMEOUT_MS;
|
|
315
|
+
start(sendPing: () => void, intervalMs?: number): void;
|
|
316
|
+
/**
|
|
317
|
+
* Abandonne le Worker et reprend le battement avec un `setInterval`.
|
|
318
|
+
* Idempotent et insensible aux cycles périmés.
|
|
319
|
+
*/
|
|
320
|
+
private _fallbackFromWorker;
|
|
321
|
+
private _startFallback;
|
|
322
|
+
private _clearWatchdog;
|
|
323
|
+
/** Termine le Worker courant et libère l'URL d'objet associée. */
|
|
324
|
+
private _disposeWorker;
|
|
325
|
+
stop(): void;
|
|
326
|
+
get isActive(): boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
212
329
|
/** Connection state machine */
|
|
213
330
|
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
|
|
214
331
|
/**
|
|
@@ -231,7 +348,7 @@ declare const HEARTBEAT_INTERVAL_MS = 25000;
|
|
|
231
348
|
*
|
|
232
349
|
* @example
|
|
233
350
|
* ```ts
|
|
234
|
-
* const aura = createClient('
|
|
351
|
+
* const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx')
|
|
235
352
|
*
|
|
236
353
|
* const channel = aura.realtime.channel('room-1')
|
|
237
354
|
* .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => {
|
|
@@ -320,6 +437,17 @@ declare class RealtimeService {
|
|
|
320
437
|
* la diffusion ni la boucle de reconnexion.
|
|
321
438
|
*/
|
|
322
439
|
private _emitError;
|
|
440
|
+
/**
|
|
441
|
+
* Préfixe `"<project_id>:"` des noms de canaux sur le fil, `""` s'il est inconnu.
|
|
442
|
+
*
|
|
443
|
+
* `aura-realtime` exige que tout canal commence par le `project_id` du TOKEN
|
|
444
|
+
* (`handlers/websocket.rs`, `expected_prefix` ; idem pour le broadcast HTTP,
|
|
445
|
+
* `handlers/broadcast.rs`). Le SDK ne porte plus d'identifiant de projet — le projet est
|
|
446
|
+
* celui de l'URL — mais le token de session, lui, porte ce claim : on le lit là, dans le
|
|
447
|
+
* même jeton que celui envoyé au serveur. Sans token (pas encore connecté), il n'y a rien
|
|
448
|
+
* à préfixer ; la connexion WS échouerait de toute façon, le serveur exigeant un JWT.
|
|
449
|
+
*/
|
|
450
|
+
private _projectPrefix;
|
|
323
451
|
private get _wsUrl();
|
|
324
452
|
/** Ensure the WebSocket connection is open. */
|
|
325
453
|
connect(): void;
|
|
@@ -380,7 +508,15 @@ declare class RealtimeService {
|
|
|
380
508
|
* puis on relance le cycle de reconnexion nous-mêmes.
|
|
381
509
|
*/
|
|
382
510
|
private _handleDeadConnection;
|
|
383
|
-
/**
|
|
511
|
+
/**
|
|
512
|
+
* Send raw data through the WebSocket.
|
|
513
|
+
*
|
|
514
|
+
* @returns `true` si le message a été remis à la socket, `false` s'il a été **jeté**
|
|
515
|
+
* (socket absente ou non-OPEN). Ce retour n'est pas cosmétique : sans lui,
|
|
516
|
+
* `AuraChannel.send()`/`track()` rendaient `'ok'` pour un message qui n'avait jamais
|
|
517
|
+
* quitté le process — la seule branche de `send()` capable de rendre `'error'` était
|
|
518
|
+
* une exception que ce `_send` ne lève jamais (C18/N24).
|
|
519
|
+
*/
|
|
384
520
|
private _send;
|
|
385
521
|
/** Refresh the JWT token on an existing connection (no disconnect). */
|
|
386
522
|
refreshToken(token: string): void;
|
|
@@ -409,18 +545,46 @@ declare class RealtimeService {
|
|
|
409
545
|
* Envoie un broadcast par HTTP (REST), sans connexion WebSocket ouverte
|
|
410
546
|
* (parité `@supabase/realtime-js`). Utile côté serveur/edge.
|
|
411
547
|
*
|
|
412
|
-
* POST `/v1/realtime/
|
|
548
|
+
* POST `/v1/realtime/broadcast` avec le contrat du handler backend
|
|
413
549
|
* (`broadcast.rs`) : `{ channel, payload }`, `channel` préfixé par le projet,
|
|
414
550
|
* `event` replié dans `payload` (aligné sur le broadcast WS).
|
|
415
551
|
*
|
|
416
|
-
*
|
|
552
|
+
* ### Le canal DOIT porter le projet — y compris hors session (K2)
|
|
553
|
+
*
|
|
554
|
+
* Le handler exige `channel.starts_with("{project_id}:")` et répond sinon
|
|
555
|
+
* `403 « Le canal n'appartient pas à ce projet »`
|
|
556
|
+
* (`services/aura-realtime/src/handlers/broadcast.rs`, fonction `broadcast`).
|
|
557
|
+
*
|
|
558
|
+
* Avec une session, ce préfixe est ajouté ici même : il est lu dans le claim
|
|
559
|
+
* `project_id` du jeton (voir {@link _projectPrefix}).
|
|
560
|
+
*
|
|
561
|
+
* **Sans session — le cas d'usage serveur/edge de cette méthode** — le SDK n'a AUCUN
|
|
562
|
+
* identifiant de projet : l'URL est la seule désignation du tenant côté client, et rien
|
|
563
|
+
* ne la traduit en `project_id` (c'est la passerelle qui fait cette résolution, à partir
|
|
564
|
+
* de l'hôte). L'authentification, elle, passe : pour le realtime, la passerelle forge un
|
|
565
|
+
* JWT interne à partir des claims de la clé API et écrase l'`Authorization`
|
|
566
|
+
* (`aura-gateway/src/proxy/sse_proxy.rs::do_http_proxy`). Émettre un canal nu produisait
|
|
567
|
+
* donc un `403` dont le message ne nomme jamais la forme attendue.
|
|
568
|
+
*
|
|
569
|
+
* Cette méthode **refuse** désormais ce cas, sans appel réseau
|
|
570
|
+
* (`error.code === 'realtime_channel_non_qualifie'`) : passez le canal déjà qualifié,
|
|
571
|
+
* `"<project_id>:<canal>"`. Cette forme est acceptée dans les deux situations — avec une
|
|
572
|
+
* session, elle n'est pas préfixée une seconde fois.
|
|
573
|
+
*
|
|
574
|
+
* @param channel Nom du canal. Avec une session : nom nu ou déjà qualifié. Sans session :
|
|
575
|
+
* OBLIGATOIREMENT `"<project_id>:<canal>"`.
|
|
417
576
|
* @param event Nom de l'événement broadcast.
|
|
418
577
|
* @param payload Données de l'événement.
|
|
419
|
-
* @returns `AuraResponse<null>` — `error` non nul en cas de 4xx/rate-limit
|
|
578
|
+
* @returns `AuraResponse<null>` — `error` non nul en cas de 4xx/rate-limit, ou de canal
|
|
579
|
+
* non qualifiable (refus local, aucun appel réseau).
|
|
420
580
|
*
|
|
421
581
|
* @example
|
|
422
582
|
* ```ts
|
|
583
|
+
* // Navigateur, utilisateur connecté : le projet vient du jeton de session.
|
|
423
584
|
* await aura.realtime.httpBroadcast('room-1', 'cursor', { x: 10, y: 20 })
|
|
585
|
+
*
|
|
586
|
+
* // Serveur / edge (clé service_role, aucune session) : qualifier le canal.
|
|
587
|
+
* await aura.realtime.httpBroadcast(`${PROJECT_ID}:room-1`, 'cursor', { x: 10, y: 20 })
|
|
424
588
|
* ```
|
|
425
589
|
*/
|
|
426
590
|
httpBroadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<AuraResponse<null>>;
|
|
@@ -453,82 +617,6 @@ declare class RealtimeService {
|
|
|
453
617
|
removeAllChannels(): void;
|
|
454
618
|
/** Route incoming server messages to the correct channel. */
|
|
455
619
|
private _dispatch;
|
|
456
|
-
/**
|
|
457
|
-
* Simple callback-based subscription (legacy API).
|
|
458
|
-
* Prefer using `.channel().on().subscribe()` instead.
|
|
459
|
-
*
|
|
460
|
-
* @deprecated Use `channel().on().subscribe()` instead.
|
|
461
|
-
*/
|
|
462
|
-
on<T = Record<string, unknown>>(channelName: string, callback: (event: any) => void): {
|
|
463
|
-
unsubscribe: () => void;
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* HeartbeatManager — pings périodiques pour maintenir la connexion WS.
|
|
469
|
-
*
|
|
470
|
-
* Utilise un Worker dédié : les navigateurs throttlent agressivement les timers
|
|
471
|
-
* d'un onglet en arrière-plan (jusqu'à 1 tick/minute), ce qui ferait manquer le
|
|
472
|
-
* `heartbeat_timeout` de 60 s du serveur ; un Worker n'est pas soumis à ce
|
|
473
|
-
* throttling. Fallback `setInterval` quand le Worker est indisponible.
|
|
474
|
-
*
|
|
475
|
-
* ── Pourquoi trois filets et pas un ──────────────────────────────────────────
|
|
476
|
-
* Ce battement ne sert plus seulement à tenir la connexion ouverte : c'est aussi
|
|
477
|
-
* lui qui cadence la **détection de connexion morte** du `RealtimeService`. S'il
|
|
478
|
-
* se tait, le client ne ping plus (le serveur coupe à 60 s) *et* ne détecte plus
|
|
479
|
-
* rien. Les trois modes de défaillance d'un Worker sont donc couverts :
|
|
480
|
-
*
|
|
481
|
-
* 1. **Échec synchrone** (`new Worker` lève, `Worker` absent) → `try/catch`.
|
|
482
|
-
* 2. **Échec asynchrone** — le cas d'une CSP `worker-src` qui n'autorise pas
|
|
483
|
-
* `blob:`. La spec HTML est explicite : quand le fetch du script échoue,
|
|
484
|
-
* le constructeur **ne lève pas**, l'agent utilisateur déclenche un événement
|
|
485
|
-
* `error` sur l'objet Worker. Un `try/catch` ne peut donc rien attraper →
|
|
486
|
-
* `onerror` bascule sur le fallback.
|
|
487
|
-
* 3. **Worker inerte** — construit sans erreur mais qui ne poste jamais rien.
|
|
488
|
-
* Aucun événement ne le signale : le Worker accuse donc réception du `start`
|
|
489
|
-
* par un `ready` immédiat, et un chien de garde bascule si cet accusé
|
|
490
|
-
* n'arrive pas.
|
|
491
|
-
*
|
|
492
|
-
* Le chien de garde porte sur le `ready`, PAS sur le premier battement : mesuré
|
|
493
|
-
* en Chromium réel, le démarrage d'un Worker peut dépasser 1,5 intervalle quand
|
|
494
|
-
* l'intervalle est court, ce qui faisait abandonner à tort un Worker
|
|
495
|
-
* parfaitement sain — et perdre silencieusement l'immunité au throttling, la
|
|
496
|
-
* seule raison d'être de ce Worker.
|
|
497
|
-
*/
|
|
498
|
-
declare class HeartbeatManager {
|
|
499
|
-
private _worker;
|
|
500
|
-
private _workerUrl;
|
|
501
|
-
private _interval;
|
|
502
|
-
private _sendFn;
|
|
503
|
-
/** Chien de garde de l'accusé `ready` du Worker (mode de défaillance 3). */
|
|
504
|
-
private _watchdog;
|
|
505
|
-
/**
|
|
506
|
-
* Numéro du cycle start/stop courant. Un événement asynchrone (erreur de
|
|
507
|
-
* Worker, chien de garde) portant un numéro périmé appartient à un cycle déjà
|
|
508
|
-
* arrêté ou remplacé : il doit être ignoré, jamais ressusciter un heartbeat.
|
|
509
|
-
*/
|
|
510
|
-
private _generation;
|
|
511
|
-
/**
|
|
512
|
-
* Délai accordé au Worker pour accuser réception du `start`.
|
|
513
|
-
*
|
|
514
|
-
* Volontairement **indépendant de l'intervalle de heartbeat** : c'est un temps
|
|
515
|
-
* de démarrage de thread, pas un multiple de la cadence. 3 s est très large
|
|
516
|
-
* pour un Worker sain (quelques millisecondes en pratique) tout en restant
|
|
517
|
-
* loin sous le `heartbeat_timeout` de 60 s du serveur.
|
|
518
|
-
*/
|
|
519
|
-
private static readonly WORKER_READY_TIMEOUT_MS;
|
|
520
|
-
start(sendPing: () => void, intervalMs?: number): void;
|
|
521
|
-
/**
|
|
522
|
-
* Abandonne le Worker et reprend le battement avec un `setInterval`.
|
|
523
|
-
* Idempotent et insensible aux cycles périmés.
|
|
524
|
-
*/
|
|
525
|
-
private _fallbackFromWorker;
|
|
526
|
-
private _startFallback;
|
|
527
|
-
private _clearWatchdog;
|
|
528
|
-
/** Termine le Worker courant et libère l'URL d'objet associée. */
|
|
529
|
-
private _disposeWorker;
|
|
530
|
-
stop(): void;
|
|
531
|
-
get isActive(): boolean;
|
|
532
620
|
}
|
|
533
621
|
|
|
534
622
|
export { AuraChannel, type ConnectionState, DEFAULT_SUBSCRIBE_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, HeartbeatManager, RealtimeConnectionError, type RealtimeErrorPhase, RealtimeService };
|