@aurabase/realtime 0.4.0 → 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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChannelConfig, PostgresChangesFilter, RealtimeEvent, BroadcastFilter, PresenceFilter, SubscribeCallback, BroadcastPayload, PresenceState, ServerMessage, RealtimeListenType, AuraClientLike, AuraResponse } from '@aurabase/core';
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
- type SendFn = (data: string) => void;
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 _projectId;
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 → resolve */
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
- constructor(name: string, projectId: string, options?: ChannelConfig);
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: any) => void): this;
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).
@@ -95,6 +120,26 @@ declare class AuraChannel {
95
120
  _bindSend(sendFn: SendFn): void;
96
121
  /** Send the subscribe message to the server. */
97
122
  _doSubscribe(): void;
123
+ /**
124
+ * Notifie le callback de statut d'abonnement, sauf si le canal est fermé.
125
+ *
126
+ * Un canal sorti par `unsubscribe()` est **soldé** : son cycle de vie s'est
127
+ * terminé sur `CLOSED`. Rien ne doit plus le faire parler — ni une erreur
128
+ * serveur globale rediffusée à tous les canaux, ni une confirmation `subscribed`
129
+ * encore en vol au moment du désabonnement. Sans cette garde, l'appelant voyait
130
+ * un `CHANNEL_ERROR` ou un `SUBSCRIBED` APRÈS le `CLOSED` de son propre canal.
131
+ *
132
+ * Parité `@supabase/realtime-js` : un canal quitté est retiré de
133
+ * `socket.channels` (`RealtimeClient#_remove`) et échappe donc par construction
134
+ * à `Socket#triggerChanError` et au routage des réponses. Le SDK Aurabase garde
135
+ * l'instance dans sa Map (`unsubscribe()` ne la retire pas, seul
136
+ * `removeChannel()` le fait), la garde est donc portée par le canal lui-même.
137
+ *
138
+ * `CLOSED` est la seule exception : il est émis précisément au moment où
139
+ * `_subscribed` bascule à `false` — d'où son émission directe dans
140
+ * `unsubscribe()`.
141
+ */
142
+ private _emitStatus;
98
143
  /** (Re)arme le compte à rebours d'attente de la confirmation `subscribed`. */
99
144
  private _armSubscribeTimeout;
100
145
  private _clearSubscribeTimeout;
@@ -111,19 +156,36 @@ declare class AuraChannel {
111
156
  /** Unsubscribe from the channel. */
112
157
  unsubscribe(): void;
113
158
  /**
114
- * Send a broadcast message to the channel.
115
- * If broadcast.ack=true, returns a Promise that resolves when the server ACKs.
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`).
116
171
  *
117
172
  * @example
118
173
  * ```ts
119
- * // Without ACK (fire & forget)
174
+ * // Sans ACK (fire & forget)
120
175
  * channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100, y: 200 } })
121
176
  *
122
- * // With ACK (wait for server confirmation)
123
- * await channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100 } })
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
+ * })
124
186
  * ```
125
187
  */
126
- send(message: BroadcastPayload): Promise<'ok' | 'timeout' | 'error'>;
188
+ send(message: BroadcastPayload): Promise<BroadcastSendStatus>;
127
189
  /**
128
190
  * Track presence state for this client.
129
191
  * Returns a Promise that resolves with 'ok' once tracked (compatible Supabase).
@@ -134,12 +196,20 @@ declare class AuraChannel {
134
196
  * ```
135
197
  */
136
198
  track(state: Record<string, unknown>): Promise<'ok' | 'error'>;
137
- /** Stop tracking presence. */
199
+ /** Stop tracking presence. Même contrat que `track()`. */
138
200
  untrack(): Promise<'ok' | 'error'>;
139
201
  /** Get the current presence state for this channel. */
140
202
  presenceState(): PresenceState;
141
203
  /** Called by RealtimeService when a server message arrives for this channel. */
142
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;
143
213
  /** Dispatch a RealtimeEvent to matching listeners. */
144
214
  private _dispatchEvent;
145
215
  /** Whether this channel has been subscribed. */
@@ -189,6 +259,73 @@ declare class RealtimeConnectionError extends Error {
189
259
  });
190
260
  }
191
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
+
192
329
  /** Connection state machine */
193
330
  type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
194
331
  /**
@@ -211,7 +348,7 @@ declare const HEARTBEAT_INTERVAL_MS = 25000;
211
348
  *
212
349
  * @example
213
350
  * ```ts
214
- * const aura = createClient('http://localhost:8080', 'my-project')
351
+ * const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx')
215
352
  *
216
353
  * const channel = aura.realtime.channel('room-1')
217
354
  * .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => {
@@ -251,6 +388,17 @@ declare class RealtimeService {
251
388
  * 30 s et répond au ping : la connexion est morte (TCP à demi-fermé).
252
389
  */
253
390
  private _awaitingPong;
391
+ /** Date du dernier message serveur reçu, pour le filet de liveness. */
392
+ private _lastServerMessageAt;
393
+ /**
394
+ * Filet de liveness, indépendant du `HeartbeatManager`.
395
+ *
396
+ * Sans lui, toute la détection de connexion morte reposait sur le tick du
397
+ * heartbeat — lui-même porté par un Worker. Un Worker tué silencieusement (par
398
+ * la pression mémoire d'un mobile, sans événement `error`) supprimait d'un coup
399
+ * le ping ET la détection : le heartbeat était un point unique de défaillance.
400
+ */
401
+ private _livenessGuard;
254
402
  /** Backoff delays in ms: 1s, 2s, 4s, 8s, 16s, 30s (cap) */
255
403
  private static readonly BACKOFF_MS;
256
404
  constructor(client: AuraClientLike);
@@ -289,6 +437,17 @@ declare class RealtimeService {
289
437
  * la diffusion ni la boucle de reconnexion.
290
438
  */
291
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;
292
451
  private get _wsUrl();
293
452
  /** Ensure the WebSocket connection is open. */
294
453
  connect(): void;
@@ -298,6 +457,37 @@ declare class RealtimeService {
298
457
  * compter sur un `onclose` : une socket à demi-fermée n'en émet parfois jamais).
299
458
  */
300
459
  private _detachSocket;
460
+ /**
461
+ * Arme le filet de liveness pour `ws` : il ne juge que sur la date du dernier
462
+ * message reçu, donc il survit à la mort complète du heartbeat.
463
+ *
464
+ * Il vit sur le thread principal, qu'un navigateur throttle en arrière-plan :
465
+ * cela ne peut que le RETARDER, jamais provoquer un faux positif — le verdict
466
+ * porte sur une durée mesurée, pas sur le nombre de ticks écoulés.
467
+ */
468
+ private _startLivenessGuard;
469
+ private _stopLivenessGuard;
470
+ /** Annule le refresh proactif du token, s'il est armé. */
471
+ private _cancelTokenRefresh;
472
+ /**
473
+ * Bascule vers l'état terminal `disconnected` : plus aucune reconnexion n'est
474
+ * prévue, le service doit donc **cesser toute activité de fond**.
475
+ *
476
+ * Le point sensible est le refresh proactif du token. Le laisser courir n'est
477
+ * pas anodin : `auth.refreshSession()` frappe le backend, qui applique une
478
+ * ROTATION STRICTE — chaque refresh révoque l'ancien refresh token
479
+ * (`services/aura-auth/src/services/auth_service.rs`, « Rotation atomique (E2) »),
480
+ * et rejouer un token révoqué au-delà de la fenêtre de grâce déclenche
481
+ * `token_reuse_detected` → **révocation globale de toutes les sessions de
482
+ * l'utilisateur**. Comme `refreshToken()` replanifie systématiquement le timer,
483
+ * un service pourtant `disconnected` faisait tourner les jetons de la session à
484
+ * perpétuité, en tâche de fond, sans que personne ne l'ait demandé.
485
+ *
486
+ * En revanche le refresh est **conservé pendant une reconnexion** : le prochain
487
+ * handshake lit `authStore.token`, et un backoff un peu long laisserait sinon
488
+ * expirer le token — le ré-handshake partirait alors en 401.
489
+ */
490
+ private _goDisconnected;
301
491
  /**
302
492
  * Un tick de heartbeat : vérifie la liveness du lien puis émet un ping.
303
493
  *
@@ -318,7 +508,15 @@ declare class RealtimeService {
318
508
  * puis on relance le cycle de reconnexion nous-mêmes.
319
509
  */
320
510
  private _handleDeadConnection;
321
- /** Send raw data through the WebSocket. */
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
+ */
322
520
  private _send;
323
521
  /** Refresh the JWT token on an existing connection (no disconnect). */
324
522
  refreshToken(token: string): void;
@@ -347,18 +545,46 @@ declare class RealtimeService {
347
545
  * Envoie un broadcast par HTTP (REST), sans connexion WebSocket ouverte
348
546
  * (parité `@supabase/realtime-js`). Utile côté serveur/edge.
349
547
  *
350
- * POST `/v1/realtime/{projectId}/broadcast` avec le contrat du handler backend
548
+ * POST `/v1/realtime/broadcast` avec le contrat du handler backend
351
549
  * (`broadcast.rs`) : `{ channel, payload }`, `channel` préfixé par le projet,
352
550
  * `event` replié dans `payload` (aligné sur le broadcast WS).
353
551
  *
354
- * @param channel Nom du canal (préfixé projet automatiquement si absent).
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>"`.
355
576
  * @param event Nom de l'événement broadcast.
356
577
  * @param payload Données de l'événement.
357
- * @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).
358
580
  *
359
581
  * @example
360
582
  * ```ts
583
+ * // Navigateur, utilisateur connecté : le projet vient du jeton de session.
361
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 })
362
588
  * ```
363
589
  */
364
590
  httpBroadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<AuraResponse<null>>;
@@ -391,29 +617,6 @@ declare class RealtimeService {
391
617
  removeAllChannels(): void;
392
618
  /** Route incoming server messages to the correct channel. */
393
619
  private _dispatch;
394
- /**
395
- * Simple callback-based subscription (legacy API).
396
- * Prefer using `.channel().on().subscribe()` instead.
397
- *
398
- * @deprecated Use `channel().on().subscribe()` instead.
399
- */
400
- on<T = Record<string, unknown>>(channelName: string, callback: (event: any) => void): {
401
- unsubscribe: () => void;
402
- };
403
- }
404
-
405
- /**
406
- * HeartbeatManager — pings périodiques pour maintenir la connexion WS.
407
- * Utilise un Worker dédié (anti-throttle des onglets en arrière-plan) ; fallback setInterval.
408
- */
409
- declare class HeartbeatManager {
410
- private _worker;
411
- private _workerUrl;
412
- private _interval;
413
- private _sendFn;
414
- start(sendPing: () => void, intervalMs?: number): void;
415
- stop(): void;
416
- get isActive(): boolean;
417
620
  }
418
621
 
419
622
  export { AuraChannel, type ConnectionState, DEFAULT_SUBSCRIBE_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, HeartbeatManager, RealtimeConnectionError, type RealtimeErrorPhase, RealtimeService };