@aurabase/realtime 0.4.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/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/index.cjs +1232 -0
- package/dist/index.d.cts +419 -0
- package/dist/index.d.ts +419 -0
- package/dist/index.js +1225 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aurabase
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# @aurabase/realtime
|
|
2
|
+
|
|
3
|
+
Service temps réel du SDK Aurabase : une connexion WebSocket multiplexée sur des channels (`postgres_changes`, `broadcast`, `presence`).
|
|
4
|
+
|
|
5
|
+
 
|
|
6
|
+
|
|
7
|
+
> Pre-release — pas encore publié sur npm.
|
|
8
|
+
|
|
9
|
+
Fait partie du SDK **[aurabase](../aurabase)**. Installé automatiquement avec `aurabase` ; ce package est publié séparément pour un usage ciblé / tree-shaking.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @aurabase/realtime
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> Dépend de `@aurabase/core`. Déjà inclus si vous utilisez le package `aurabase` — cette installation ciblée sert à composer votre propre client ou à limiter la taille du bundle. Nécessite un runtime avec `WebSocket` global (navigateur, ou Node avec un polyfill).
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
`RealtimeService` est exposé par le client principal sous `aura.realtime`, et `aura.channel(name)` est un raccourci pour `aura.realtime.channel(name)`.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createClient } from '@aurabase/aurabase-js'
|
|
25
|
+
|
|
26
|
+
const aura = createClient('http://localhost:8080', 'my-project', 'aura_anon_xxx')
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Connexion
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const unsubscribe = aura.realtime.onStateChange((state) => {
|
|
33
|
+
console.log('realtime state:', state) // 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
aura.realtime.connect()
|
|
37
|
+
console.log(aura.realtime.connectionState)
|
|
38
|
+
|
|
39
|
+
// Désactive la reconnexion auto (backoff + jitter), ou borne le nombre de tentatives.
|
|
40
|
+
aura.realtime.setAutoReconnect(true, 5)
|
|
41
|
+
|
|
42
|
+
// Pousse un nouveau token sur la connexion existante (sans reconnexion).
|
|
43
|
+
aura.realtime.refreshToken('new-access-token')
|
|
44
|
+
|
|
45
|
+
// Erreurs de transport (handshake refusé, coupure, connexion morte).
|
|
46
|
+
const off = aura.realtime.onError((err) => {
|
|
47
|
+
console.error(err.phase, err.code, err.message)
|
|
48
|
+
// err.phase : 'handshake' | 'connection' | 'liveness'
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Heartbeat et détection de connexion morte
|
|
53
|
+
|
|
54
|
+
Un heartbeat applicatif (`{"type":"ping"}`) est envoyé toutes les **25 secondes** via un Worker
|
|
55
|
+
dédié, pour éviter le throttling des timers par les navigateurs en arrière-plan.
|
|
56
|
+
|
|
57
|
+
Ce heartbeat sert aussi de **détecteur de liveness** : le serveur répond immédiatement au ping et
|
|
58
|
+
pousse de lui-même un `pong` toutes les 30 s, donc si aucun message n'est revenu entre deux ticks,
|
|
59
|
+
la connexion est déclarée morte — même quand le système la croit encore ouverte (TCP à demi-fermé
|
|
60
|
+
sur réseau mobile ou derrière un proxy, cas où aucun `close` n'est jamais émis). Le SDK détache
|
|
61
|
+
alors la socket, notifie via `onError` (`phase: 'liveness'`) et `CHANNEL_ERROR` sur chaque canal,
|
|
62
|
+
puis relance la reconnexion automatique (backoff exponentiel avec jitter).
|
|
63
|
+
|
|
64
|
+
### Erreurs d'authentification à la connexion
|
|
65
|
+
|
|
66
|
+
Un handshake refusé par le serveur (token JWT absent/expiré/invalide → HTTP 401) est signalé via
|
|
67
|
+
`onError` avec `phase: 'handshake'`, et remonté aux canaux en `CHANNEL_ERROR`. La spécification
|
|
68
|
+
WebSocket n'expose pas le statut HTTP du handshake au client (l'événement `error` est vide, le code
|
|
69
|
+
de fermeture vaut toujours `1006`) : le SDK nomme donc la phase et les causes possibles, sans
|
|
70
|
+
prétendre distinguer un 401 d'un 429 ou d'un serveur injoignable.
|
|
71
|
+
|
|
72
|
+
### `postgres_changes`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const channel = aura.channel('db:public.posts')
|
|
76
|
+
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) => {
|
|
77
|
+
console.log('insert:', payload)
|
|
78
|
+
})
|
|
79
|
+
.subscribe((status, err) => {
|
|
80
|
+
console.log(status, err)
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Statuts émis par le callback de `subscribe()` (mêmes noms et même sémantique que
|
|
85
|
+
`@supabase/realtime-js`) :
|
|
86
|
+
|
|
87
|
+
| Statut | Quand |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `SUBSCRIBED` | le serveur a confirmé l'abonnement |
|
|
90
|
+
| `CHANNEL_ERROR` | refus serveur (RLS, rate limit, canal invalide), échec de handshake, coupure inattendue ou connexion morte — `err` porte le détail |
|
|
91
|
+
| `TIMED_OUT` | aucune confirmation dans les 10 s suivant l'envoi du subscribe (délai réglable : `subscribe(cb, timeoutMs)`) |
|
|
92
|
+
| `CLOSED` | fermeture volontaire du canal : `unsubscribe()`, `removeChannel()`, `removeAllChannels()`, `disconnect()` |
|
|
93
|
+
|
|
94
|
+
Le compte à rebours de `TIMED_OUT` ne démarre qu'à l'envoi effectif du subscribe : tant que la
|
|
95
|
+
connexion n'est pas ouverte, aucun timeout n'est émis.
|
|
96
|
+
|
|
97
|
+
## Filtres de colonne (`postgres_changes`)
|
|
98
|
+
|
|
99
|
+
`postgres_changes` accepte un champ `filter` (syntaxe PostgREST/Supabase : `"column=operator.value"`) pour ne recevoir que les events dont la ligne matche la condition :
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
aura.channel('room')
|
|
103
|
+
.on('postgres_changes', {
|
|
104
|
+
event: 'INSERT',
|
|
105
|
+
schema: 'public',
|
|
106
|
+
table: 'messages',
|
|
107
|
+
filter: 'room_id=eq.42',
|
|
108
|
+
}, (e) => console.log(e))
|
|
109
|
+
.subscribe()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Opérateurs supportés : `eq, neq, lt, lte, gt, gte, in, like, ilike, is, match, imatch, isdistinct`.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// Comparaison numérique.
|
|
116
|
+
{ event: '*', schema: 'public', table: 'items', filter: 'qty=gt.5' }
|
|
117
|
+
|
|
118
|
+
// Appartenance à un ensemble — les virgules internes à `in.(...)` ne
|
|
119
|
+
// séparent PAS des conditions AND (voir plus bas).
|
|
120
|
+
{ event: '*', schema: 'public', table: 'orders', filter: 'status=in.(open,pending)' }
|
|
121
|
+
|
|
122
|
+
// Motif SQL LIKE (% / _).
|
|
123
|
+
{ event: '*', schema: 'public', table: 'users', filter: 'name=like.jo%' }
|
|
124
|
+
|
|
125
|
+
// Négation — préfixe `not.` devant l'opérateur.
|
|
126
|
+
{ event: '*', schema: 'public', table: 'orders', filter: 'status=not.eq.done' }
|
|
127
|
+
|
|
128
|
+
// AND par virgules — chaque condition séparée par `,` (hors `in.(...)`).
|
|
129
|
+
{ event: '*', schema: 'public', table: 'items', filter: 'room_id=eq.42,qty=gt.5' }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
À savoir :
|
|
133
|
+
|
|
134
|
+
- **Le serveur `aura-realtime` est autoritaire** : il applique déjà le filtre avant d'envoyer l'event sur le socket. Le client réapplique la même logique en second rideau (defense-in-depth) — il ne peut que sur-filtrer, jamais laisser passer un event que le serveur aurait dû exclure.
|
|
135
|
+
- **Fail-closed** : une condition non parsable (syntaxe invalide, opérateur inconnu, trop de valeurs `in`, pattern `match`/`imatch` trop long) exclut l'event plutôt que de le laisser passer.
|
|
136
|
+
- **DELETE** : le filtre est évalué sur **l'ancienne ligne** (`old_record`), pas sur la nouvelle (qui n'existe plus). Les colonnes disponibles dans `old_record` dépendent du `REPLICA IDENTITY` de la table (`DEFAULT` : seule la clé primaire ; `FULL` : toutes les colonnes) — un filtre sur une colonne absente de `old_record` échoue à matcher (résultat non évaluable ⇒ exclusion, jamais inversée par `not.`), **sauf `is.null` et `isdistinct`** qui traitent une colonne absente comme `NULL` et peuvent donc matcher même si la colonne n'est pas présente dans `old_record`.
|
|
137
|
+
- `schema` (et `table`) doivent correspondre exactement au schéma/table réels tels qu'envoyés par le serveur dans l'event — un `schema` incorrect exclut silencieusement tous les events de ce channel.
|
|
138
|
+
|
|
139
|
+
### Broadcast
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const room = aura.realtime.channel('room-1', { broadcast: { ack: true, self: true } })
|
|
143
|
+
|
|
144
|
+
room
|
|
145
|
+
.on('broadcast', { event: 'cursor-pos' }, (payload) => {
|
|
146
|
+
console.log(payload)
|
|
147
|
+
})
|
|
148
|
+
.subscribe()
|
|
149
|
+
|
|
150
|
+
// 'ok' | 'timeout' | 'error' — résout à l'ACK serveur si broadcast.ack est activé sur le channel.
|
|
151
|
+
const status = await room.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100, y: 200 } })
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Broadcast HTTP (sans WebSocket)
|
|
155
|
+
|
|
156
|
+
Émet un broadcast via REST, sans connexion WebSocket ouverte — utile côté serveur/edge.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// POST /v1/realtime/{projectId}/broadcast — pas besoin de connect().
|
|
160
|
+
const { error } = await aura.realtime.httpBroadcast('room-1', 'cursor-pos', { x: 10, y: 20 })
|
|
161
|
+
if (error) console.error(error) // 4xx / rate-limit surfacés proprement
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Le nom de canal est préfixé par le projet automatiquement s'il ne l'est pas déjà.
|
|
165
|
+
|
|
166
|
+
### Canaux privés (`setAuth`)
|
|
167
|
+
|
|
168
|
+
Les canaux privés (`{ private: true }`) sont autorisés par les policies RLS côté serveur à
|
|
169
|
+
partir du JWT courant. Fixez/rafraîchissez ce token via `setAuth()` **avant** de vous abonner :
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Utilise le token fourni (ou, sans argument, reprend client.authStore.token).
|
|
173
|
+
aura.realtime.setAuth(session.access_token)
|
|
174
|
+
|
|
175
|
+
const secret = aura.realtime
|
|
176
|
+
.channel('secret-room', { private: true })
|
|
177
|
+
.on('broadcast', { event: 'msg' }, console.log)
|
|
178
|
+
.subscribe((status, err) => {
|
|
179
|
+
if (status === 'CHANNEL_ERROR') console.error('Refusé par la policy RLS', err)
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`setAuth()` propage `{ type: 'access_token' }` sur une connexion ouverte (dédupliqué, sans
|
|
184
|
+
reconnexion). Un token non autorisé par la policy provoque un `CHANNEL_ERROR` à la souscription.
|
|
185
|
+
|
|
186
|
+
### Presence
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const lobby = aura.channel('room-1', { config: { presence: { key: 'user-1' } } })
|
|
190
|
+
|
|
191
|
+
lobby
|
|
192
|
+
.on('presence', { event: 'sync' }, () => {
|
|
193
|
+
console.log(lobby.presenceState())
|
|
194
|
+
})
|
|
195
|
+
.on('presence', { event: 'join' }, console.log)
|
|
196
|
+
.on('presence', { event: 'leave' }, console.log)
|
|
197
|
+
.subscribe()
|
|
198
|
+
|
|
199
|
+
await lobby.track({ online_at: new Date().toISOString() })
|
|
200
|
+
await lobby.untrack()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Nettoyage
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
aura.realtime.getChannels()
|
|
207
|
+
aura.realtime.removeChannel('room-1')
|
|
208
|
+
aura.realtime.removeAllChannels()
|
|
209
|
+
aura.realtime.disconnect()
|
|
210
|
+
unsubscribe()
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Limitations
|
|
214
|
+
|
|
215
|
+
- Pas de limitation de débit configurable (`eventsPerSecond`) sur le broadcast (côté client).
|
|
216
|
+
- La reconnexion réabonne les channels existants mais ne garantit pas la livraison des événements manqués pendant la coupure (au-delà du rejeu par `since_id` quand le serveur le supporte).
|
|
217
|
+
|
|
218
|
+
## Documentation
|
|
219
|
+
|
|
220
|
+
Référence complète du SDK (tous les services, types partagés, SSR) : [README racine](../../README.md).
|