@aurabase/realtime 0.5.3 → 0.6.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.cjs CHANGED
@@ -226,6 +226,7 @@ var AuraChannel = class {
226
226
  name;
227
227
  _projectPrefix;
228
228
  _listeners = [];
229
+ _listenerIdCounter = 0;
229
230
  _subscribed = false;
230
231
  _sendFn = null;
231
232
  _subscribeCallback = null;
@@ -287,8 +288,69 @@ var AuraChannel = class {
287
288
  get fullName() {
288
289
  return `${this._projectPrefix()}${this.name}`;
289
290
  }
291
+ /**
292
+ * Signature d'implémentation de `.on()`.
293
+ * Retourne une fonction de désabonnement direct `() => void` qui proxy également l'instance du canal pour le chaînage fluide.
294
+ */
290
295
  on(type, filter, callback) {
291
- this._listeners.push({ type, filter, callback });
296
+ const id = ++this._listenerIdCounter;
297
+ this._listeners.push({
298
+ id,
299
+ type,
300
+ filter,
301
+ callback
302
+ });
303
+ const unsubscribe = () => {
304
+ const idx = this._listeners.findIndex((l) => l.id === id);
305
+ if (idx !== -1) {
306
+ this._listeners.splice(idx, 1);
307
+ }
308
+ };
309
+ return new Proxy(unsubscribe, {
310
+ get: (_target, prop) => {
311
+ const val = this[prop];
312
+ if (typeof val === "function") {
313
+ return (...args) => Reflect.apply(val, this, args);
314
+ }
315
+ return val;
316
+ }
317
+ });
318
+ }
319
+ /**
320
+ * Supprime un ou plusieurs listeners du canal sans clore le canal ni couper le WebSocket.
321
+ *
322
+ * - `off(type, filter, callback)` : supprime le listener exact.
323
+ * - `off(type, filter)` : supprime les listeners correspondant à ce type et ce filtre.
324
+ * - `off(type)` : supprime tous les listeners du type spécifié.
325
+ * - `off()` : supprime tous les listeners locaux du canal.
326
+ */
327
+ off(type, filter, callback) {
328
+ if (!type) {
329
+ this._listeners.length = 0;
330
+ return this;
331
+ }
332
+ const filtered = this._listeners.filter((l) => {
333
+ if (l.type !== type) return true;
334
+ if (filter) {
335
+ const filterMeta = filter;
336
+ const listenerFilter = l.filter;
337
+ if ("event" in filter && "event" in l.filter && listenerFilter.event !== filterMeta.event) {
338
+ return true;
339
+ }
340
+ if ("schema" in filter && listenerFilter.schema !== filterMeta.schema) {
341
+ return true;
342
+ }
343
+ if ("table" in filter && listenerFilter.table !== filterMeta.table) {
344
+ return true;
345
+ }
346
+ }
347
+ if (callback && l.callback !== callback) {
348
+ return true;
349
+ }
350
+ return false;
351
+ });
352
+ this._listeners.length = 0;
353
+ this._listeners.push(...filtered);
292
354
  return this;
293
355
  }
294
356
  // ─── Subscribe ──────────────────────────────────────────────────────────
@@ -698,32 +760,57 @@ var AuraChannel = class {
698
760
  }
699
761
  }
700
762
  const cdcPayload = payload && typeof payload === "object" ? payload : {};
701
- if (cdcPayload.new === void 0 && cdcPayload.record !== void 0) {
702
- cdcPayload.new = cdcPayload.record;
703
- }
704
- if (cdcPayload.old === void 0 && cdcPayload.old_record !== void 0) {
705
- cdcPayload.old = cdcPayload.old_record;
706
- }
707
- if (cdcPayload.eventType === void 0 && cdcPayload.type !== void 0) {
708
- cdcPayload.eventType = cdcPayload.type;
709
- }
710
- const enrichedEvent = Object.assign(Object.create(event), {
711
- new: cdcPayload.record,
712
- old: cdcPayload.old_record,
713
- eventType: cdcPayload.type,
714
- schema: cdcPayload.schema,
715
- table: cdcPayload.table
716
- });
763
+ const rawRecord = cdcPayload.record ?? cdcPayload.new ?? null;
764
+ const rawOldRecord = cdcPayload.old_record ?? cdcPayload.old ?? null;
765
+ const rawType = cdcPayload.type ?? cdcPayload.eventType;
766
+ const eventTypeString = typeof rawType === "string" ? rawType : typeof eventType === "string" ? eventType : "INSERT";
767
+ const cdcType = eventTypeString.toUpperCase();
768
+ const recordNew = cdcType === "DELETE" ? null : rawRecord;
769
+ const recordOld = cdcType === "INSERT" ? null : rawOldRecord;
770
+ const enrichedPayload = {
771
+ ...cdcPayload,
772
+ record: rawRecord,
773
+ old_record: rawOldRecord,
774
+ new: recordNew,
775
+ old: recordOld,
776
+ eventType: cdcType,
777
+ schema: cdcPayload.schema ?? filter.schema ?? "public",
778
+ table: cdcPayload.table ?? filter.table ?? "",
779
+ commit_timestamp: cdcPayload.commit_timestamp ?? event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
780
+ };
781
+ const enrichedEvent = {
782
+ ...event,
783
+ schema: cdcPayload.schema ?? filter.schema ?? "public",
784
+ table: cdcPayload.table ?? filter.table ?? "",
785
+ commit_timestamp: cdcPayload.commit_timestamp ?? event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
786
+ errors: cdcPayload.errors,
787
+ id: event.id,
788
+ topic: event.topic,
789
+ timestamp: event.timestamp,
790
+ eventType: cdcType,
791
+ event: cdcType === "INSERT" ? "insert" : cdcType === "UPDATE" ? "update" : "delete",
792
+ new: recordNew,
793
+ old: recordOld,
794
+ payload: enrichedPayload
795
+ };
717
796
  listener.callback(enrichedEvent);
718
797
  }
719
798
  }
720
799
  } else if (listener.type === "broadcast") {
721
800
  if (eventType === "broadcast") {
722
801
  const filter = listener.filter;
723
- const broadcastPayload = event.payload && typeof event.payload === "object" ? event.payload : void 0;
724
- const broadcastEvent = broadcastPayload?.event;
802
+ const broadcastPayload = event.payload && typeof event.payload === "object" ? event.payload : {};
803
+ const broadcastEvent = broadcastPayload.event ?? "broadcast";
725
804
  if (!filter.event || filter.event === "*" || broadcastEvent === filter.event) {
726
- listener.callback(event);
805
+ const enrichedBroadcast = {
806
+ id: event.id,
807
+ topic: event.topic,
808
+ timestamp: event.timestamp,
809
+ allowed_sub_ids: event.allowed_sub_ids,
810
+ event: broadcastEvent,
811
+ payload: broadcastPayload
812
+ };
813
+ listener.callback(enrichedBroadcast);
727
814
  }
728
815
  }
729
816
  } else if (listener.type === "presence") {
@@ -1372,8 +1459,25 @@ var RealtimeService = class _RealtimeService {
1372
1459
  * await aura.realtime.httpBroadcast(`${PROJECT_ID}:room-1`, 'cursor', { x: 10, y: 20 })
1373
1460
  * ```
1374
1461
  */
1375
- httpBroadcast(channel, event, payload = {}) {
1376
- const prefix = this._projectPrefix();
1462
+ /**
1463
+ * Envoie un broadcast par HTTP (REST), sans connexion WebSocket ouverte
1464
+ * (parité `@supabase/realtime-js`). Utile côté serveur/edge.
1465
+ *
1466
+ * POST `/v1/realtime/broadcast` avec le contrat du handler backend
1467
+ * (`broadcast.rs`) : `{ channel, payload }`, `channel` préfixé par le projet,
1468
+ * `event` replié dans `payload` (aligné sur le broadcast WS).
1469
+ *
1470
+ * Le canal peut être fourni :
1471
+ * - sous forme courte (`'room-1'`) si une session utilisateur est présente,
1472
+ * - sous forme courte avec l'option `{ projectId: '<uuid>' }`,
1473
+ * - ou déjà qualifié (`'<project_id>:room-1'`).
1474
+ */
1475
+ httpBroadcast(channel, event, payload = {}, options) {
1476
+ const explicitProjectId = options?.projectId ?? this.client.projectId;
1477
+ let prefix = this._projectPrefix();
1478
+ if (!prefix && explicitProjectId) {
1479
+ prefix = explicitProjectId.endsWith(":") ? explicitProjectId : `${explicitProjectId}:`;
1480
+ }
1377
1481
  let fullChannel;
1378
1482
  if (prefix) {
1379
1483
  fullChannel = channel.startsWith(prefix) ? channel : `${prefix}${channel}`;
@@ -1385,7 +1489,7 @@ var RealtimeService = class _RealtimeService {
1385
1489
  error: new core.AuraError(0, {
1386
1490
  error: {
1387
1491
  code: "realtime_channel_non_qualifie",
1388
- message: `httpBroadcast: le canal "${channel}" n'est pas qualifi\xE9 et aucune session n'est ouverte \u2014 le SDK n'a donc aucun \`project_id\` \xE0 lui pr\xE9fixer, alors que le serveur l'EXIGE (403 \xAB Le canal n'appartient pas \xE0 ce projet \xBB). Passez le canal sous la forme "<project_id>:` + channel + `" \u2014 c'est la forme attendue c\xF4t\xE9 serveur/edge, o\xF9 il n'y a pas de session utilisateur.`
1492
+ message: `httpBroadcast: le canal "${channel}" n'est pas qualifi\xE9 et aucune session n'est ouverte \u2014 le SDK n'a donc aucun \`project_id\` \xE0 lui pr\xE9fixer, alors que le serveur l'EXIGE (403 \xAB Le canal n'appartient pas \xE0 ce projet \xBB). Passez le canal sous la forme "<project_id>:` + channel + `" ou sp\xE9cifiez l'option { projectId: "<uuid>" } \u2014 c'est la forme attendue c\xF4t\xE9 serveur/edge, o\xF9 il n'y a pas de session utilisateur.`
1389
1493
  }
1390
1494
  })
1391
1495
  });
package/dist/index.d.cts CHANGED
@@ -1,14 +1,21 @@
1
- import { ChannelConfig, PostgresChangesFilter, RealtimeEvent, BroadcastFilter, PresenceFilter, PresenceState, SubscribeCallback, BroadcastPayload, BroadcastSendStatus, ServerMessage, AuraClientLike, AuraResponse } from '@aurabase/core';
1
+ import { ChannelConfig, PostgresChangesFilter, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, RealtimePostgresChangesPayload, BroadcastFilter, RealtimeBroadcastEvent, PresenceFilter, PresenceState, RealtimeEvent, RealtimeListenType, SubscribeCallback, BroadcastPayload, BroadcastSendStatus, ServerMessage, AuraClientLike, AuraResponse } from '@aurabase/core';
2
2
 
3
+ /**
4
+ * Fonction de désabonnement retournée par `.on()`, enrichie avec les méthodes du canal pour permettre le chaînage fluide.
5
+ */
6
+ type AuraChannelSubscription = (() => void) & AuraChannel;
3
7
  type ListenerEntry = {
8
+ id: number;
4
9
  type: 'postgres_changes';
5
10
  filter: PostgresChangesFilter;
6
- callback: (payload: RealtimeEvent) => void;
11
+ callback: (payload: RealtimePostgresChangesPayload<Record<string, unknown>>) => void;
7
12
  } | {
13
+ id: number;
8
14
  type: 'broadcast';
9
15
  filter: BroadcastFilter;
10
- callback: (payload: RealtimeEvent) => void;
16
+ callback: (payload: RealtimeBroadcastEvent<Record<string, unknown>>) => void;
11
17
  } | {
18
+ id: number;
12
19
  type: 'presence';
13
20
  filter: PresenceFilter;
14
21
  callback: (payload: PresenceState | RealtimeEvent | Record<string, unknown>) => void;
@@ -52,6 +59,7 @@ declare class AuraChannel {
52
59
  readonly name: string;
53
60
  private readonly _projectPrefix;
54
61
  private readonly _listeners;
62
+ private _listenerIdCounter;
55
63
  private _subscribed;
56
64
  private _sendFn;
57
65
  private _subscribeCallback;
@@ -101,12 +109,49 @@ declare class AuraChannel {
101
109
  /** Full channel name with project prefix (for server protocol) */
102
110
  get fullName(): string;
103
111
  /**
104
- * Register a listener for a specific event type.
105
- * Returns `this` for chaining.
112
+ * Enregistre un listener pour les événements PostgreSQL (CDC) avec filtrage INSERT.
113
+ * Typé spécifiquement avec `new: TRow` et `old: null`.
114
+ */
115
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter & {
116
+ event: 'INSERT';
117
+ }, callback: (payload: RealtimePostgresInsertPayload<TRow>) => void): AuraChannelSubscription;
118
+ /**
119
+ * Enregistre un listener pour les événements PostgreSQL (CDC) avec filtrage UPDATE.
120
+ * Typé spécifiquement avec `new: TRow` et `old: Partial<TRow>`.
121
+ */
122
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter & {
123
+ event: 'UPDATE';
124
+ }, callback: (payload: RealtimePostgresUpdatePayload<TRow>) => void): AuraChannelSubscription;
125
+ /**
126
+ * Enregistre un listener pour les événements PostgreSQL (CDC) avec filtrage DELETE.
127
+ * Typé spécifiquement avec `new: null` et `old: Partial<TRow> | null`.
128
+ */
129
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter & {
130
+ event: 'DELETE';
131
+ }, callback: (payload: RealtimePostgresDeletePayload<TRow>) => void): AuraChannelSubscription;
132
+ /**
133
+ * Enregistre un listener pour les événements PostgreSQL (CDC) (INSERT, UPDATE, DELETE ou '*').
134
+ * Typé comme union discriminée par `eventType`.
135
+ */
136
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter, callback: (payload: RealtimePostgresChangesPayload<TRow>) => void): AuraChannelSubscription;
137
+ /**
138
+ * Enregistre un listener broadcast avec typage générique du payload applicatif.
139
+ * Permet l'accès direct et typé à `({ payload }) => payload.x` ainsi qu'aux métadonnées.
106
140
  */
107
- on(type: 'postgres_changes', filter: PostgresChangesFilter, callback: (payload: RealtimeEvent) => void): this;
108
- on(type: 'broadcast', filter: BroadcastFilter, callback: (payload: RealtimeEvent) => void): this;
109
- on(type: 'presence', filter: PresenceFilter, callback: (payload: PresenceState | RealtimeEvent | Record<string, unknown>) => void): this;
141
+ on<TPayload = Record<string, unknown>>(type: 'broadcast', filter: BroadcastFilter, callback: (payload: RealtimeBroadcastEvent<TPayload>) => void): AuraChannelSubscription;
142
+ /**
143
+ * Enregistre un listener pour la présence.
144
+ */
145
+ on(type: 'presence', filter: PresenceFilter, callback: (payload: PresenceState | RealtimeEvent | Record<string, unknown>) => void): AuraChannelSubscription;
146
+ /**
147
+ * Supprime un ou plusieurs listeners du canal sans clore le canal ni couper le WebSocket.
148
+ *
149
+ * - `off(type, filter, callback)` : supprime le listener exact.
150
+ * - `off(type, filter)` : supprime les listeners correspondant à ce type et ce filtre.
151
+ * - `off(type)` : supprime tous les listeners du type spécifié.
152
+ * - `off()` : supprime tous les listeners locaux du canal.
153
+ */
154
+ off(type?: RealtimeListenType, filter?: PostgresChangesFilter | BroadcastFilter | PresenceFilter, callback?: (payload: unknown) => void): this;
110
155
  /**
111
156
  * Subscribe to the channel. Sends the Subscribe message to the server
112
157
  * with the full config (postgres_changes, broadcast, presence).
@@ -371,6 +416,13 @@ declare const HEARTBEAT_INTERVAL_MS = 25000;
371
416
  * })
372
417
  * ```
373
418
  */
419
+ interface HttpBroadcastOptions {
420
+ /**
421
+ * Identifiant UUID du projet pour qualifier le canal lorsque le SDK fonctionne sans session
422
+ * utilisateur (ex: côté serveur ou edge avec une clé API service_role).
423
+ */
424
+ projectId?: string;
425
+ }
374
426
  declare class RealtimeService {
375
427
  private readonly client;
376
428
  private _ws;
@@ -596,7 +648,20 @@ declare class RealtimeService {
596
648
  * await aura.realtime.httpBroadcast(`${PROJECT_ID}:room-1`, 'cursor', { x: 10, y: 20 })
597
649
  * ```
598
650
  */
599
- httpBroadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<AuraResponse<null>>;
651
+ /**
652
+ * Envoie un broadcast par HTTP (REST), sans connexion WebSocket ouverte
653
+ * (parité `@supabase/realtime-js`). Utile côté serveur/edge.
654
+ *
655
+ * POST `/v1/realtime/broadcast` avec le contrat du handler backend
656
+ * (`broadcast.rs`) : `{ channel, payload }`, `channel` préfixé par le projet,
657
+ * `event` replié dans `payload` (aligné sur le broadcast WS).
658
+ *
659
+ * Le canal peut être fourni :
660
+ * - sous forme courte (`'room-1'`) si une session utilisateur est présente,
661
+ * - sous forme courte avec l'option `{ projectId: '<uuid>' }`,
662
+ * - ou déjà qualifié (`'<project_id>:room-1'`).
663
+ */
664
+ httpBroadcast(channel: string, event: string, payload?: Record<string, unknown>, options?: HttpBroadcastOptions): Promise<AuraResponse<null>>;
600
665
  /**
601
666
  * Schedule proactive token refresh at 80% of the JWT TTL.
602
667
  * Prevents token expiration during an active WebSocket session.
@@ -628,4 +693,4 @@ declare class RealtimeService {
628
693
  private _dispatch;
629
694
  }
630
695
 
631
- export { AuraChannel, type ConnectionState, DEFAULT_SUBSCRIBE_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, HeartbeatManager, RealtimeConnectionError, type RealtimeErrorPhase, RealtimeService };
696
+ export { AuraChannel, type AuraChannelSubscription, type ConnectionState, DEFAULT_SUBSCRIBE_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, HeartbeatManager, type HttpBroadcastOptions, RealtimeConnectionError, type RealtimeErrorPhase, RealtimeService };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,21 @@
1
- import { ChannelConfig, PostgresChangesFilter, RealtimeEvent, BroadcastFilter, PresenceFilter, PresenceState, SubscribeCallback, BroadcastPayload, BroadcastSendStatus, ServerMessage, AuraClientLike, AuraResponse } from '@aurabase/core';
1
+ import { ChannelConfig, PostgresChangesFilter, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, RealtimePostgresChangesPayload, BroadcastFilter, RealtimeBroadcastEvent, PresenceFilter, PresenceState, RealtimeEvent, RealtimeListenType, SubscribeCallback, BroadcastPayload, BroadcastSendStatus, ServerMessage, AuraClientLike, AuraResponse } from '@aurabase/core';
2
2
 
3
+ /**
4
+ * Fonction de désabonnement retournée par `.on()`, enrichie avec les méthodes du canal pour permettre le chaînage fluide.
5
+ */
6
+ type AuraChannelSubscription = (() => void) & AuraChannel;
3
7
  type ListenerEntry = {
8
+ id: number;
4
9
  type: 'postgres_changes';
5
10
  filter: PostgresChangesFilter;
6
- callback: (payload: RealtimeEvent) => void;
11
+ callback: (payload: RealtimePostgresChangesPayload<Record<string, unknown>>) => void;
7
12
  } | {
13
+ id: number;
8
14
  type: 'broadcast';
9
15
  filter: BroadcastFilter;
10
- callback: (payload: RealtimeEvent) => void;
16
+ callback: (payload: RealtimeBroadcastEvent<Record<string, unknown>>) => void;
11
17
  } | {
18
+ id: number;
12
19
  type: 'presence';
13
20
  filter: PresenceFilter;
14
21
  callback: (payload: PresenceState | RealtimeEvent | Record<string, unknown>) => void;
@@ -52,6 +59,7 @@ declare class AuraChannel {
52
59
  readonly name: string;
53
60
  private readonly _projectPrefix;
54
61
  private readonly _listeners;
62
+ private _listenerIdCounter;
55
63
  private _subscribed;
56
64
  private _sendFn;
57
65
  private _subscribeCallback;
@@ -101,12 +109,49 @@ declare class AuraChannel {
101
109
  /** Full channel name with project prefix (for server protocol) */
102
110
  get fullName(): string;
103
111
  /**
104
- * Register a listener for a specific event type.
105
- * Returns `this` for chaining.
112
+ * Enregistre un listener pour les événements PostgreSQL (CDC) avec filtrage INSERT.
113
+ * Typé spécifiquement avec `new: TRow` et `old: null`.
114
+ */
115
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter & {
116
+ event: 'INSERT';
117
+ }, callback: (payload: RealtimePostgresInsertPayload<TRow>) => void): AuraChannelSubscription;
118
+ /**
119
+ * Enregistre un listener pour les événements PostgreSQL (CDC) avec filtrage UPDATE.
120
+ * Typé spécifiquement avec `new: TRow` et `old: Partial<TRow>`.
121
+ */
122
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter & {
123
+ event: 'UPDATE';
124
+ }, callback: (payload: RealtimePostgresUpdatePayload<TRow>) => void): AuraChannelSubscription;
125
+ /**
126
+ * Enregistre un listener pour les événements PostgreSQL (CDC) avec filtrage DELETE.
127
+ * Typé spécifiquement avec `new: null` et `old: Partial<TRow> | null`.
128
+ */
129
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter & {
130
+ event: 'DELETE';
131
+ }, callback: (payload: RealtimePostgresDeletePayload<TRow>) => void): AuraChannelSubscription;
132
+ /**
133
+ * Enregistre un listener pour les événements PostgreSQL (CDC) (INSERT, UPDATE, DELETE ou '*').
134
+ * Typé comme union discriminée par `eventType`.
135
+ */
136
+ on<TRow = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter, callback: (payload: RealtimePostgresChangesPayload<TRow>) => void): AuraChannelSubscription;
137
+ /**
138
+ * Enregistre un listener broadcast avec typage générique du payload applicatif.
139
+ * Permet l'accès direct et typé à `({ payload }) => payload.x` ainsi qu'aux métadonnées.
106
140
  */
107
- on(type: 'postgres_changes', filter: PostgresChangesFilter, callback: (payload: RealtimeEvent) => void): this;
108
- on(type: 'broadcast', filter: BroadcastFilter, callback: (payload: RealtimeEvent) => void): this;
109
- on(type: 'presence', filter: PresenceFilter, callback: (payload: PresenceState | RealtimeEvent | Record<string, unknown>) => void): this;
141
+ on<TPayload = Record<string, unknown>>(type: 'broadcast', filter: BroadcastFilter, callback: (payload: RealtimeBroadcastEvent<TPayload>) => void): AuraChannelSubscription;
142
+ /**
143
+ * Enregistre un listener pour la présence.
144
+ */
145
+ on(type: 'presence', filter: PresenceFilter, callback: (payload: PresenceState | RealtimeEvent | Record<string, unknown>) => void): AuraChannelSubscription;
146
+ /**
147
+ * Supprime un ou plusieurs listeners du canal sans clore le canal ni couper le WebSocket.
148
+ *
149
+ * - `off(type, filter, callback)` : supprime le listener exact.
150
+ * - `off(type, filter)` : supprime les listeners correspondant à ce type et ce filtre.
151
+ * - `off(type)` : supprime tous les listeners du type spécifié.
152
+ * - `off()` : supprime tous les listeners locaux du canal.
153
+ */
154
+ off(type?: RealtimeListenType, filter?: PostgresChangesFilter | BroadcastFilter | PresenceFilter, callback?: (payload: unknown) => void): this;
110
155
  /**
111
156
  * Subscribe to the channel. Sends the Subscribe message to the server
112
157
  * with the full config (postgres_changes, broadcast, presence).
@@ -371,6 +416,13 @@ declare const HEARTBEAT_INTERVAL_MS = 25000;
371
416
  * })
372
417
  * ```
373
418
  */
419
+ interface HttpBroadcastOptions {
420
+ /**
421
+ * Identifiant UUID du projet pour qualifier le canal lorsque le SDK fonctionne sans session
422
+ * utilisateur (ex: côté serveur ou edge avec une clé API service_role).
423
+ */
424
+ projectId?: string;
425
+ }
374
426
  declare class RealtimeService {
375
427
  private readonly client;
376
428
  private _ws;
@@ -596,7 +648,20 @@ declare class RealtimeService {
596
648
  * await aura.realtime.httpBroadcast(`${PROJECT_ID}:room-1`, 'cursor', { x: 10, y: 20 })
597
649
  * ```
598
650
  */
599
- httpBroadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<AuraResponse<null>>;
651
+ /**
652
+ * Envoie un broadcast par HTTP (REST), sans connexion WebSocket ouverte
653
+ * (parité `@supabase/realtime-js`). Utile côté serveur/edge.
654
+ *
655
+ * POST `/v1/realtime/broadcast` avec le contrat du handler backend
656
+ * (`broadcast.rs`) : `{ channel, payload }`, `channel` préfixé par le projet,
657
+ * `event` replié dans `payload` (aligné sur le broadcast WS).
658
+ *
659
+ * Le canal peut être fourni :
660
+ * - sous forme courte (`'room-1'`) si une session utilisateur est présente,
661
+ * - sous forme courte avec l'option `{ projectId: '<uuid>' }`,
662
+ * - ou déjà qualifié (`'<project_id>:room-1'`).
663
+ */
664
+ httpBroadcast(channel: string, event: string, payload?: Record<string, unknown>, options?: HttpBroadcastOptions): Promise<AuraResponse<null>>;
600
665
  /**
601
666
  * Schedule proactive token refresh at 80% of the JWT TTL.
602
667
  * Prevents token expiration during an active WebSocket session.
@@ -628,4 +693,4 @@ declare class RealtimeService {
628
693
  private _dispatch;
629
694
  }
630
695
 
631
- export { AuraChannel, type ConnectionState, DEFAULT_SUBSCRIBE_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, HeartbeatManager, RealtimeConnectionError, type RealtimeErrorPhase, RealtimeService };
696
+ export { AuraChannel, type AuraChannelSubscription, type ConnectionState, DEFAULT_SUBSCRIBE_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS, HeartbeatManager, type HttpBroadcastOptions, RealtimeConnectionError, type RealtimeErrorPhase, RealtimeService };
package/dist/index.js CHANGED
@@ -224,6 +224,7 @@ var AuraChannel = class {
224
224
  name;
225
225
  _projectPrefix;
226
226
  _listeners = [];
227
+ _listenerIdCounter = 0;
227
228
  _subscribed = false;
228
229
  _sendFn = null;
229
230
  _subscribeCallback = null;
@@ -285,8 +286,69 @@ var AuraChannel = class {
285
286
  get fullName() {
286
287
  return `${this._projectPrefix()}${this.name}`;
287
288
  }
289
+ /**
290
+ * Signature d'implémentation de `.on()`.
291
+ * Retourne une fonction de désabonnement direct `() => void` qui proxy également l'instance du canal pour le chaînage fluide.
292
+ */
288
293
  on(type, filter, callback) {
289
- this._listeners.push({ type, filter, callback });
294
+ const id = ++this._listenerIdCounter;
295
+ this._listeners.push({
296
+ id,
297
+ type,
298
+ filter,
299
+ callback
300
+ });
301
+ const unsubscribe = () => {
302
+ const idx = this._listeners.findIndex((l) => l.id === id);
303
+ if (idx !== -1) {
304
+ this._listeners.splice(idx, 1);
305
+ }
306
+ };
307
+ return new Proxy(unsubscribe, {
308
+ get: (_target, prop) => {
309
+ const val = this[prop];
310
+ if (typeof val === "function") {
311
+ return (...args) => Reflect.apply(val, this, args);
312
+ }
313
+ return val;
314
+ }
315
+ });
316
+ }
317
+ /**
318
+ * Supprime un ou plusieurs listeners du canal sans clore le canal ni couper le WebSocket.
319
+ *
320
+ * - `off(type, filter, callback)` : supprime le listener exact.
321
+ * - `off(type, filter)` : supprime les listeners correspondant à ce type et ce filtre.
322
+ * - `off(type)` : supprime tous les listeners du type spécifié.
323
+ * - `off()` : supprime tous les listeners locaux du canal.
324
+ */
325
+ off(type, filter, callback) {
326
+ if (!type) {
327
+ this._listeners.length = 0;
328
+ return this;
329
+ }
330
+ const filtered = this._listeners.filter((l) => {
331
+ if (l.type !== type) return true;
332
+ if (filter) {
333
+ const filterMeta = filter;
334
+ const listenerFilter = l.filter;
335
+ if ("event" in filter && "event" in l.filter && listenerFilter.event !== filterMeta.event) {
336
+ return true;
337
+ }
338
+ if ("schema" in filter && listenerFilter.schema !== filterMeta.schema) {
339
+ return true;
340
+ }
341
+ if ("table" in filter && listenerFilter.table !== filterMeta.table) {
342
+ return true;
343
+ }
344
+ }
345
+ if (callback && l.callback !== callback) {
346
+ return true;
347
+ }
348
+ return false;
349
+ });
350
+ this._listeners.length = 0;
351
+ this._listeners.push(...filtered);
290
352
  return this;
291
353
  }
292
354
  // ─── Subscribe ──────────────────────────────────────────────────────────
@@ -696,32 +758,57 @@ var AuraChannel = class {
696
758
  }
697
759
  }
698
760
  const cdcPayload = payload && typeof payload === "object" ? payload : {};
699
- if (cdcPayload.new === void 0 && cdcPayload.record !== void 0) {
700
- cdcPayload.new = cdcPayload.record;
701
- }
702
- if (cdcPayload.old === void 0 && cdcPayload.old_record !== void 0) {
703
- cdcPayload.old = cdcPayload.old_record;
704
- }
705
- if (cdcPayload.eventType === void 0 && cdcPayload.type !== void 0) {
706
- cdcPayload.eventType = cdcPayload.type;
707
- }
708
- const enrichedEvent = Object.assign(Object.create(event), {
709
- new: cdcPayload.record,
710
- old: cdcPayload.old_record,
711
- eventType: cdcPayload.type,
712
- schema: cdcPayload.schema,
713
- table: cdcPayload.table
714
- });
761
+ const rawRecord = cdcPayload.record ?? cdcPayload.new ?? null;
762
+ const rawOldRecord = cdcPayload.old_record ?? cdcPayload.old ?? null;
763
+ const rawType = cdcPayload.type ?? cdcPayload.eventType;
764
+ const eventTypeString = typeof rawType === "string" ? rawType : typeof eventType === "string" ? eventType : "INSERT";
765
+ const cdcType = eventTypeString.toUpperCase();
766
+ const recordNew = cdcType === "DELETE" ? null : rawRecord;
767
+ const recordOld = cdcType === "INSERT" ? null : rawOldRecord;
768
+ const enrichedPayload = {
769
+ ...cdcPayload,
770
+ record: rawRecord,
771
+ old_record: rawOldRecord,
772
+ new: recordNew,
773
+ old: recordOld,
774
+ eventType: cdcType,
775
+ schema: cdcPayload.schema ?? filter.schema ?? "public",
776
+ table: cdcPayload.table ?? filter.table ?? "",
777
+ commit_timestamp: cdcPayload.commit_timestamp ?? event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
778
+ };
779
+ const enrichedEvent = {
780
+ ...event,
781
+ schema: cdcPayload.schema ?? filter.schema ?? "public",
782
+ table: cdcPayload.table ?? filter.table ?? "",
783
+ commit_timestamp: cdcPayload.commit_timestamp ?? event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
784
+ errors: cdcPayload.errors,
785
+ id: event.id,
786
+ topic: event.topic,
787
+ timestamp: event.timestamp,
788
+ eventType: cdcType,
789
+ event: cdcType === "INSERT" ? "insert" : cdcType === "UPDATE" ? "update" : "delete",
790
+ new: recordNew,
791
+ old: recordOld,
792
+ payload: enrichedPayload
793
+ };
715
794
  listener.callback(enrichedEvent);
716
795
  }
717
796
  }
718
797
  } else if (listener.type === "broadcast") {
719
798
  if (eventType === "broadcast") {
720
799
  const filter = listener.filter;
721
- const broadcastPayload = event.payload && typeof event.payload === "object" ? event.payload : void 0;
722
- const broadcastEvent = broadcastPayload?.event;
800
+ const broadcastPayload = event.payload && typeof event.payload === "object" ? event.payload : {};
801
+ const broadcastEvent = broadcastPayload.event ?? "broadcast";
723
802
  if (!filter.event || filter.event === "*" || broadcastEvent === filter.event) {
724
- listener.callback(event);
803
+ const enrichedBroadcast = {
804
+ id: event.id,
805
+ topic: event.topic,
806
+ timestamp: event.timestamp,
807
+ allowed_sub_ids: event.allowed_sub_ids,
808
+ event: broadcastEvent,
809
+ payload: broadcastPayload
810
+ };
811
+ listener.callback(enrichedBroadcast);
725
812
  }
726
813
  }
727
814
  } else if (listener.type === "presence") {
@@ -1370,8 +1457,25 @@ var RealtimeService = class _RealtimeService {
1370
1457
  * await aura.realtime.httpBroadcast(`${PROJECT_ID}:room-1`, 'cursor', { x: 10, y: 20 })
1371
1458
  * ```
1372
1459
  */
1373
- httpBroadcast(channel, event, payload = {}) {
1374
- const prefix = this._projectPrefix();
1460
+ /**
1461
+ * Envoie un broadcast par HTTP (REST), sans connexion WebSocket ouverte
1462
+ * (parité `@supabase/realtime-js`). Utile côté serveur/edge.
1463
+ *
1464
+ * POST `/v1/realtime/broadcast` avec le contrat du handler backend
1465
+ * (`broadcast.rs`) : `{ channel, payload }`, `channel` préfixé par le projet,
1466
+ * `event` replié dans `payload` (aligné sur le broadcast WS).
1467
+ *
1468
+ * Le canal peut être fourni :
1469
+ * - sous forme courte (`'room-1'`) si une session utilisateur est présente,
1470
+ * - sous forme courte avec l'option `{ projectId: '<uuid>' }`,
1471
+ * - ou déjà qualifié (`'<project_id>:room-1'`).
1472
+ */
1473
+ httpBroadcast(channel, event, payload = {}, options) {
1474
+ const explicitProjectId = options?.projectId ?? this.client.projectId;
1475
+ let prefix = this._projectPrefix();
1476
+ if (!prefix && explicitProjectId) {
1477
+ prefix = explicitProjectId.endsWith(":") ? explicitProjectId : `${explicitProjectId}:`;
1478
+ }
1375
1479
  let fullChannel;
1376
1480
  if (prefix) {
1377
1481
  fullChannel = channel.startsWith(prefix) ? channel : `${prefix}${channel}`;
@@ -1383,7 +1487,7 @@ var RealtimeService = class _RealtimeService {
1383
1487
  error: new AuraError(0, {
1384
1488
  error: {
1385
1489
  code: "realtime_channel_non_qualifie",
1386
- message: `httpBroadcast: le canal "${channel}" n'est pas qualifi\xE9 et aucune session n'est ouverte \u2014 le SDK n'a donc aucun \`project_id\` \xE0 lui pr\xE9fixer, alors que le serveur l'EXIGE (403 \xAB Le canal n'appartient pas \xE0 ce projet \xBB). Passez le canal sous la forme "<project_id>:` + channel + `" \u2014 c'est la forme attendue c\xF4t\xE9 serveur/edge, o\xF9 il n'y a pas de session utilisateur.`
1490
+ message: `httpBroadcast: le canal "${channel}" n'est pas qualifi\xE9 et aucune session n'est ouverte \u2014 le SDK n'a donc aucun \`project_id\` \xE0 lui pr\xE9fixer, alors que le serveur l'EXIGE (403 \xAB Le canal n'appartient pas \xE0 ce projet \xBB). Passez le canal sous la forme "<project_id>:` + channel + `" ou sp\xE9cifiez l'option { projectId: "<uuid>" } \u2014 c'est la forme attendue c\xF4t\xE9 serveur/edge, o\xF9 il n'y a pas de session utilisateur.`
1387
1491
  }
1388
1492
  })
1389
1493
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurabase/realtime",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Service temps réel du SDK Aurabase (postgres_changes, broadcast, presence)",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -45,7 +45,7 @@
45
45
  "realtime"
46
46
  ],
47
47
  "dependencies": {
48
- "@aurabase/core": "0.5.3"
48
+ "@aurabase/core": "0.6.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "20.14.15",