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