@fluxbase/sdk 0.0.1-rc.27 → 0.0.1-rc.28

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.js CHANGED
@@ -663,10 +663,14 @@ var FluxbaseAuth = class {
663
663
 
664
664
  // src/realtime.ts
665
665
  var RealtimeChannel = class {
666
- constructor(url, channelName, token = null) {
666
+ constructor(url, channelName, token = null, config = {}) {
667
667
  this.ws = null;
668
668
  this.callbacks = /* @__PURE__ */ new Map();
669
+ this.presenceCallbacks = /* @__PURE__ */ new Map();
670
+ this.broadcastCallbacks = /* @__PURE__ */ new Map();
669
671
  this.subscriptionConfig = null;
672
+ this._presenceState = {};
673
+ this.myPresenceKey = null;
670
674
  this.reconnectAttempts = 0;
671
675
  this.maxReconnectAttempts = 10;
672
676
  this.reconnectDelay = 1e3;
@@ -674,6 +678,7 @@ var RealtimeChannel = class {
674
678
  this.url = url;
675
679
  this.channelName = channelName;
676
680
  this.token = token;
681
+ this.config = config;
677
682
  }
678
683
  // Implementation
679
684
  on(event, configOrCallback, callback) {
@@ -686,6 +691,20 @@ var RealtimeChannel = class {
686
691
  this.callbacks.set(eventType, /* @__PURE__ */ new Set());
687
692
  }
688
693
  this.callbacks.get(eventType).add(actualCallback);
694
+ } else if (event === "broadcast" && typeof configOrCallback !== "function") {
695
+ const config = configOrCallback;
696
+ const actualCallback = callback;
697
+ if (!this.broadcastCallbacks.has(config.event)) {
698
+ this.broadcastCallbacks.set(config.event, /* @__PURE__ */ new Set());
699
+ }
700
+ this.broadcastCallbacks.get(config.event).add(actualCallback);
701
+ } else if (event === "presence" && typeof configOrCallback !== "function") {
702
+ const config = configOrCallback;
703
+ const actualCallback = callback;
704
+ if (!this.presenceCallbacks.has(config.event)) {
705
+ this.presenceCallbacks.set(config.event, /* @__PURE__ */ new Set());
706
+ }
707
+ this.presenceCallbacks.get(config.event).add(actualCallback);
689
708
  } else {
690
709
  const actualEvent = event;
691
710
  const actualCallback = configOrCallback;
@@ -735,7 +754,7 @@ var RealtimeChannel = class {
735
754
  async unsubscribe(timeout) {
736
755
  return new Promise((resolve) => {
737
756
  if (this.ws) {
738
- this.send({
757
+ this.sendMessage({
739
758
  type: "unsubscribe",
740
759
  channel: this.channelName
741
760
  });
@@ -758,6 +777,131 @@ var RealtimeChannel = class {
758
777
  }
759
778
  });
760
779
  }
780
+ /**
781
+ * Send a broadcast message to all subscribers on this channel
782
+ *
783
+ * @param message - Broadcast message with type, event, and payload
784
+ * @returns Promise resolving to status
785
+ *
786
+ * @example
787
+ * ```typescript
788
+ * await channel.send({
789
+ * type: 'broadcast',
790
+ * event: 'cursor-pos',
791
+ * payload: { x: 100, y: 200 }
792
+ * })
793
+ * ```
794
+ */
795
+ async send(message) {
796
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
797
+ return "error";
798
+ }
799
+ try {
800
+ this.ws.send(
801
+ JSON.stringify({
802
+ type: "broadcast",
803
+ channel: this.channelName,
804
+ event: message.event,
805
+ payload: message.payload
806
+ })
807
+ );
808
+ if (this.config.broadcast?.ack) {
809
+ return "ok";
810
+ }
811
+ return "ok";
812
+ } catch (error) {
813
+ console.error("[Fluxbase Realtime] Failed to send broadcast:", error);
814
+ return "error";
815
+ }
816
+ }
817
+ /**
818
+ * Track user presence on this channel
819
+ *
820
+ * @param state - Presence state to track
821
+ * @returns Promise resolving to status
822
+ *
823
+ * @example
824
+ * ```typescript
825
+ * await channel.track({
826
+ * user_id: 123,
827
+ * status: 'online'
828
+ * })
829
+ * ```
830
+ */
831
+ async track(state) {
832
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
833
+ return "error";
834
+ }
835
+ try {
836
+ if (!this.myPresenceKey) {
837
+ this.myPresenceKey = this.config.presence?.key || `presence-${Math.random().toString(36).substr(2, 9)}`;
838
+ }
839
+ this.ws.send(
840
+ JSON.stringify({
841
+ type: "presence",
842
+ channel: this.channelName,
843
+ event: "track",
844
+ payload: {
845
+ key: this.myPresenceKey,
846
+ state
847
+ }
848
+ })
849
+ );
850
+ return "ok";
851
+ } catch (error) {
852
+ console.error("[Fluxbase Realtime] Failed to track presence:", error);
853
+ return "error";
854
+ }
855
+ }
856
+ /**
857
+ * Stop tracking presence on this channel
858
+ *
859
+ * @returns Promise resolving to status
860
+ *
861
+ * @example
862
+ * ```typescript
863
+ * await channel.untrack()
864
+ * ```
865
+ */
866
+ async untrack() {
867
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
868
+ return "error";
869
+ }
870
+ if (!this.myPresenceKey) {
871
+ return "ok";
872
+ }
873
+ try {
874
+ this.ws.send(
875
+ JSON.stringify({
876
+ type: "presence",
877
+ channel: this.channelName,
878
+ event: "untrack",
879
+ payload: {
880
+ key: this.myPresenceKey
881
+ }
882
+ })
883
+ );
884
+ this.myPresenceKey = null;
885
+ return "ok";
886
+ } catch (error) {
887
+ console.error("[Fluxbase Realtime] Failed to untrack presence:", error);
888
+ return "error";
889
+ }
890
+ }
891
+ /**
892
+ * Get current presence state for all users on this channel
893
+ *
894
+ * @returns Current presence state
895
+ *
896
+ * @example
897
+ * ```typescript
898
+ * const state = channel.presenceState()
899
+ * console.log('Online users:', Object.keys(state).length)
900
+ * ```
901
+ */
902
+ presenceState() {
903
+ return { ...this._presenceState };
904
+ }
761
905
  /**
762
906
  * Internal: Connect to WebSocket
763
907
  */
@@ -782,7 +926,7 @@ var RealtimeChannel = class {
782
926
  if (this.subscriptionConfig) {
783
927
  subscribeMessage.config = this.subscriptionConfig;
784
928
  }
785
- this.send(subscribeMessage);
929
+ this.sendMessage(subscribeMessage);
786
930
  this.startHeartbeat();
787
931
  };
788
932
  this.ws.onmessage = (event) => {
@@ -815,7 +959,7 @@ var RealtimeChannel = class {
815
959
  /**
816
960
  * Internal: Send a message
817
961
  */
818
- send(message) {
962
+ sendMessage(message) {
819
963
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
820
964
  this.ws.send(JSON.stringify(message));
821
965
  }
@@ -826,11 +970,18 @@ var RealtimeChannel = class {
826
970
  handleMessage(message) {
827
971
  switch (message.type) {
828
972
  case "heartbeat":
829
- this.send({ type: "heartbeat" });
973
+ this.ws?.send(JSON.stringify({ type: "heartbeat" }));
830
974
  break;
831
975
  case "broadcast":
832
- if (message.payload) {
833
- this.handleBroadcast(message.payload);
976
+ if (message.broadcast) {
977
+ this.handleBroadcastMessage(message.broadcast);
978
+ } else if (message.payload) {
979
+ this.handlePostgresChanges(message.payload);
980
+ }
981
+ break;
982
+ case "presence":
983
+ if (message.presence) {
984
+ this.handlePresenceMessage(message.presence);
834
985
  }
835
986
  break;
836
987
  case "ack":
@@ -844,7 +995,48 @@ var RealtimeChannel = class {
844
995
  /**
845
996
  * Internal: Handle broadcast message
846
997
  */
847
- handleBroadcast(payload) {
998
+ handleBroadcastMessage(message) {
999
+ const event = message.event;
1000
+ const payload = {
1001
+ event,
1002
+ payload: message.payload
1003
+ };
1004
+ if (!this.config.broadcast?.self && message.self) {
1005
+ return;
1006
+ }
1007
+ const callbacks = this.broadcastCallbacks.get(event);
1008
+ if (callbacks) {
1009
+ callbacks.forEach((callback) => callback(payload));
1010
+ }
1011
+ const wildcardCallbacks = this.broadcastCallbacks.get("*");
1012
+ if (wildcardCallbacks) {
1013
+ wildcardCallbacks.forEach((callback) => callback(payload));
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Internal: Handle presence message
1018
+ */
1019
+ handlePresenceMessage(message) {
1020
+ const event = message.event;
1021
+ const payload = {
1022
+ event,
1023
+ key: message.key,
1024
+ newPresences: message.newPresences,
1025
+ leftPresences: message.leftPresences,
1026
+ currentPresences: message.currentPresences || this._presenceState
1027
+ };
1028
+ if (message.currentPresences) {
1029
+ this._presenceState = message.currentPresences;
1030
+ }
1031
+ const callbacks = this.presenceCallbacks.get(event);
1032
+ if (callbacks) {
1033
+ callbacks.forEach((callback) => callback(payload));
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Internal: Handle postgres_changes message
1038
+ */
1039
+ handlePostgresChanges(payload) {
848
1040
  const supabasePayload = {
849
1041
  eventType: payload.type || payload.eventType,
850
1042
  schema: payload.schema,
@@ -868,7 +1060,7 @@ var RealtimeChannel = class {
868
1060
  */
869
1061
  startHeartbeat() {
870
1062
  this.heartbeatInterval = setInterval(() => {
871
- this.send({ type: "heartbeat" });
1063
+ this.sendMessage({ type: "heartbeat" });
872
1064
  }, 3e4);
873
1065
  }
874
1066
  /**
@@ -905,17 +1097,57 @@ var FluxbaseRealtime = class {
905
1097
  this.token = token;
906
1098
  }
907
1099
  /**
908
- * Create or get a channel
1100
+ * Create or get a channel with optional configuration
1101
+ *
909
1102
  * @param channelName - Channel name (e.g., 'table:public.products')
1103
+ * @param config - Optional channel configuration
1104
+ * @returns RealtimeChannel instance
1105
+ *
1106
+ * @example
1107
+ * ```typescript
1108
+ * const channel = realtime.channel('room-1', {
1109
+ * broadcast: { self: true, ack: true },
1110
+ * presence: { key: 'user-123' }
1111
+ * })
1112
+ * ```
910
1113
  */
911
- channel(channelName) {
912
- if (this.channels.has(channelName)) {
913
- return this.channels.get(channelName);
1114
+ channel(channelName, config) {
1115
+ const configKey = config ? JSON.stringify(config) : "";
1116
+ const key = `${channelName}:${configKey}`;
1117
+ if (this.channels.has(key)) {
1118
+ return this.channels.get(key);
914
1119
  }
915
- const channel = new RealtimeChannel(this.url, channelName, this.token);
916
- this.channels.set(channelName, channel);
1120
+ const channel = new RealtimeChannel(
1121
+ this.url,
1122
+ channelName,
1123
+ this.token,
1124
+ config
1125
+ );
1126
+ this.channels.set(key, channel);
917
1127
  return channel;
918
1128
  }
1129
+ /**
1130
+ * Remove a specific channel
1131
+ *
1132
+ * @param channel - The channel to remove
1133
+ * @returns Promise resolving to status
1134
+ *
1135
+ * @example
1136
+ * ```typescript
1137
+ * const channel = realtime.channel('room-1')
1138
+ * await realtime.removeChannel(channel)
1139
+ * ```
1140
+ */
1141
+ async removeChannel(channel) {
1142
+ await channel.unsubscribe();
1143
+ for (const [key, ch] of this.channels.entries()) {
1144
+ if (ch === channel) {
1145
+ this.channels.delete(key);
1146
+ return "ok";
1147
+ }
1148
+ }
1149
+ return "error";
1150
+ }
919
1151
  /**
920
1152
  * Remove all channels
921
1153
  */
@@ -925,8 +1157,9 @@ var FluxbaseRealtime = class {
925
1157
  }
926
1158
  /**
927
1159
  * Update auth token for all channels
1160
+ * @param token - The new auth token
928
1161
  */
929
- setToken(token) {
1162
+ setAuth(token) {
930
1163
  this.token = token;
931
1164
  }
932
1165
  };
@@ -4363,7 +4596,7 @@ var FluxbaseClient = class {
4363
4596
  const originalSetAuthToken = this.fetch.setAuthToken.bind(this.fetch);
4364
4597
  this.fetch.setAuthToken = (token) => {
4365
4598
  originalSetAuthToken(token);
4366
- this.realtime.setToken(token);
4599
+ this.realtime.setAuth(token);
4367
4600
  };
4368
4601
  }
4369
4602
  /**
@@ -4387,37 +4620,48 @@ var FluxbaseClient = class {
4387
4620
  */
4388
4621
  setAuthToken(token) {
4389
4622
  this.fetch.setAuthToken(token);
4390
- this.realtime.setToken(token);
4623
+ this.realtime.setAuth(token);
4391
4624
  }
4392
4625
  /**
4393
- * Create or get a realtime channel (Supabase-compatible alias)
4394
- *
4395
- * This is a convenience method that delegates to client.realtime.channel().
4396
- * Both patterns work identically:
4397
- * - client.channel('room-1') - Supabase-style
4398
- * - client.realtime.channel('room-1') - Fluxbase-style
4626
+ * Create or get a realtime channel (Supabase-compatible)
4399
4627
  *
4400
4628
  * @param name - Channel name
4629
+ * @param config - Optional channel configuration
4401
4630
  * @returns RealtimeChannel instance
4402
4631
  *
4403
4632
  * @example
4404
4633
  * ```typescript
4405
- * // Supabase-compatible usage
4406
- * const channel = client.channel('room-1')
4407
- * .on('postgres_changes', {
4408
- * event: '*',
4409
- * schema: 'public',
4410
- * table: 'messages'
4411
- * }, (payload) => {
4412
- * console.log('Change:', payload)
4634
+ * const channel = client.channel('room-1', {
4635
+ * broadcast: { self: true },
4636
+ * presence: { key: 'user-123' }
4637
+ * })
4638
+ * .on('broadcast', { event: 'message' }, (payload) => {
4639
+ * console.log('Message:', payload)
4413
4640
  * })
4414
4641
  * .subscribe()
4415
4642
  * ```
4416
4643
  *
4417
4644
  * @category Realtime
4418
4645
  */
4419
- channel(name) {
4420
- return this.realtime.channel(name);
4646
+ channel(name, config) {
4647
+ return this.realtime.channel(name, config);
4648
+ }
4649
+ /**
4650
+ * Remove a realtime channel (Supabase-compatible)
4651
+ *
4652
+ * @param channel - The channel to remove
4653
+ * @returns Promise resolving to status
4654
+ *
4655
+ * @example
4656
+ * ```typescript
4657
+ * const channel = client.channel('room-1')
4658
+ * await client.removeChannel(channel)
4659
+ * ```
4660
+ *
4661
+ * @category Realtime
4662
+ */
4663
+ removeChannel(channel) {
4664
+ return this.realtime.removeChannel(channel);
4421
4665
  }
4422
4666
  /**
4423
4667
  * Get the internal HTTP client