@blinkdotnew/sdk 0.14.4 → 0.14.6

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/README.md CHANGED
@@ -955,6 +955,9 @@ blink.analytics.clearAttribution()
955
955
 
956
956
  ### Realtime Operations
957
957
 
958
+ **🎉 Zero-Boilerplate Connection Management!**
959
+ All connection states, queuing, and reconnection are handled automatically. No more "CONNECTING state" errors!
960
+
958
961
  ```typescript
959
962
  // 🔥 Real-time Messaging & Presence (NEW!)
960
963
  // Perfect for chat apps, live collaboration, multiplayer games, and live updates
@@ -1263,6 +1266,70 @@ All `{{secret_name}}` placeholders are replaced with encrypted values from your
1263
1266
 
1264
1267
  ## 🌍 Framework Examples
1265
1268
 
1269
+ ### React + Realtime Connections
1270
+
1271
+ **⚠️ Critical: Avoid Multiple WebSocket Connections**
1272
+
1273
+ ```typescript
1274
+ // ❌ WRONG - Creates new connection on every user change
1275
+ useEffect(() => {
1276
+ const channel = blink.realtime.channel('room')
1277
+ await channel.subscribe({ userId: user.id, metadata: { name: user.name } })
1278
+ return () => channel.unsubscribe()
1279
+ }, [user]) // ❌ Full user object dependency causes reconnections
1280
+ ```
1281
+
1282
+ ```typescript
1283
+ // ✅ CORRECT - Single stable connection
1284
+ const userRef = useRef(user)
1285
+ const [isConnected, setIsConnected] = useState(false)
1286
+
1287
+ useEffect(() => { userRef.current = user }, [user])
1288
+
1289
+ useEffect(() => {
1290
+ if (!user?.id) return
1291
+
1292
+ const channel = blink.realtime.channel('room')
1293
+ let isMounted = true, isSubscribed = false
1294
+
1295
+ const setup = async () => {
1296
+ if (!isMounted) return
1297
+ try {
1298
+ await channel.subscribe({
1299
+ userId: userRef.current.id,
1300
+ metadata: { name: userRef.current.name }
1301
+ })
1302
+ if (!isMounted) return
1303
+ isSubscribed = true
1304
+ setIsConnected(true)
1305
+
1306
+ channel.onMessage((msg) => {
1307
+ if (!isMounted) return
1308
+ // handle message
1309
+ })
1310
+ } catch (error) {
1311
+ console.error('Connection failed:', error)
1312
+ }
1313
+ }
1314
+
1315
+ setup()
1316
+ return () => {
1317
+ isMounted = false
1318
+ setIsConnected(false)
1319
+ if (isSubscribed) channel.unsubscribe()
1320
+ }
1321
+ }, [user.id]) // ✅ Only user.id dependency
1322
+
1323
+ // Publish works automatically - no connection checks needed!
1324
+ await blink.realtime.publish('room', 'message', data)
1325
+ ```
1326
+
1327
+ **Rules:**
1328
+ 1. **useEffect dependency**: `[user.id]` not `[user]`
1329
+ 2. **Use useRef**: Store user data to avoid re-connections
1330
+ 3. **Add isMounted**: Prevent operations on unmounted components
1331
+ 4. **Zero connection management**: SDK handles all connection states automatically
1332
+
1266
1333
  ### React
1267
1334
 
1268
1335
  ```typescript
package/dist/index.d.mts CHANGED
@@ -469,6 +469,7 @@ interface RealtimeChannel {
469
469
  before?: string;
470
470
  after?: string;
471
471
  }): Promise<RealtimeMessage[]>;
472
+ isReady(): boolean;
472
473
  }
473
474
  interface RealtimeSubscribeOptions {
474
475
  userId?: string;
@@ -1512,10 +1513,19 @@ declare class BlinkRealtimeChannel implements RealtimeChannel {
1512
1513
  private presenceCallbacks;
1513
1514
  private websocket;
1514
1515
  private isSubscribed;
1516
+ private isConnected;
1517
+ private isConnecting;
1515
1518
  private reconnectTimer;
1516
1519
  private heartbeatTimer;
1517
1520
  private reconnectAttempts;
1521
+ private messageQueue;
1522
+ private pendingSubscription;
1523
+ private connectionPromise;
1518
1524
  constructor(channelName: string, httpClient: HttpClient, projectId: string);
1525
+ /**
1526
+ * Check if channel is ready for publishing
1527
+ */
1528
+ isReady(): boolean;
1519
1529
  subscribe(options?: {
1520
1530
  userId?: string;
1521
1531
  metadata?: Record<string, any>;
@@ -1533,7 +1543,27 @@ declare class BlinkRealtimeChannel implements RealtimeChannel {
1533
1543
  before?: string;
1534
1544
  after?: string;
1535
1545
  }): Promise<RealtimeMessage[]>;
1546
+ /**
1547
+ * Ensure WebSocket connection is established and ready
1548
+ */
1549
+ private ensureConnected;
1550
+ /**
1551
+ * Send a message, queuing if socket not ready
1552
+ */
1553
+ private sendMessage;
1554
+ /**
1555
+ * Send a queued message and set up response handling
1556
+ */
1557
+ private sendQueuedMessage;
1558
+ /**
1559
+ * Flush all queued messages when connection becomes ready
1560
+ */
1561
+ private flushMessageQueue;
1536
1562
  private connectWebSocket;
1563
+ /**
1564
+ * Reject all queued messages with the given error
1565
+ */
1566
+ private rejectQueuedMessages;
1537
1567
  private handleWebSocketMessage;
1538
1568
  private startHeartbeat;
1539
1569
  private scheduleReconnect;
package/dist/index.d.ts CHANGED
@@ -469,6 +469,7 @@ interface RealtimeChannel {
469
469
  before?: string;
470
470
  after?: string;
471
471
  }): Promise<RealtimeMessage[]>;
472
+ isReady(): boolean;
472
473
  }
473
474
  interface RealtimeSubscribeOptions {
474
475
  userId?: string;
@@ -1512,10 +1513,19 @@ declare class BlinkRealtimeChannel implements RealtimeChannel {
1512
1513
  private presenceCallbacks;
1513
1514
  private websocket;
1514
1515
  private isSubscribed;
1516
+ private isConnected;
1517
+ private isConnecting;
1515
1518
  private reconnectTimer;
1516
1519
  private heartbeatTimer;
1517
1520
  private reconnectAttempts;
1521
+ private messageQueue;
1522
+ private pendingSubscription;
1523
+ private connectionPromise;
1518
1524
  constructor(channelName: string, httpClient: HttpClient, projectId: string);
1525
+ /**
1526
+ * Check if channel is ready for publishing
1527
+ */
1528
+ isReady(): boolean;
1519
1529
  subscribe(options?: {
1520
1530
  userId?: string;
1521
1531
  metadata?: Record<string, any>;
@@ -1533,7 +1543,27 @@ declare class BlinkRealtimeChannel implements RealtimeChannel {
1533
1543
  before?: string;
1534
1544
  after?: string;
1535
1545
  }): Promise<RealtimeMessage[]>;
1546
+ /**
1547
+ * Ensure WebSocket connection is established and ready
1548
+ */
1549
+ private ensureConnected;
1550
+ /**
1551
+ * Send a message, queuing if socket not ready
1552
+ */
1553
+ private sendMessage;
1554
+ /**
1555
+ * Send a queued message and set up response handling
1556
+ */
1557
+ private sendQueuedMessage;
1558
+ /**
1559
+ * Flush all queued messages when connection becomes ready
1560
+ */
1561
+ private flushMessageQueue;
1536
1562
  private connectWebSocket;
1563
+ /**
1564
+ * Reject all queued messages with the given error
1565
+ */
1566
+ private rejectQueuedMessages;
1537
1567
  private handleWebSocketMessage;
1538
1568
  private startHeartbeat;
1539
1569
  private scheduleReconnect;
package/dist/index.js CHANGED
@@ -2751,40 +2751,73 @@ var BlinkRealtimeChannel = class {
2751
2751
  presenceCallbacks = [];
2752
2752
  websocket = null;
2753
2753
  isSubscribed = false;
2754
+ isConnected = false;
2755
+ isConnecting = false;
2754
2756
  reconnectTimer = null;
2755
2757
  heartbeatTimer = null;
2756
2758
  reconnectAttempts = 0;
2759
+ // Message queuing for when socket is not ready
2760
+ messageQueue = [];
2761
+ pendingSubscription = null;
2762
+ // Connection promise for awaiting readiness
2763
+ connectionPromise = null;
2764
+ /**
2765
+ * Check if channel is ready for publishing
2766
+ */
2767
+ isReady() {
2768
+ return this.isConnected && this.isSubscribed;
2769
+ }
2757
2770
  async subscribe(options = {}) {
2758
2771
  if (this.isSubscribed) {
2759
2772
  return;
2760
2773
  }
2761
- try {
2762
- await this.connectWebSocket();
2763
- if (this.websocket) {
2764
- const subscribeMessage = {
2765
- type: "subscribe",
2766
- payload: {
2767
- channel: this.channelName,
2768
- userId: options.userId,
2769
- metadata: options.metadata
2770
- }
2771
- };
2772
- this.websocket.send(JSON.stringify(subscribeMessage));
2773
- this.isSubscribed = true;
2774
- this.startHeartbeat();
2774
+ await this.ensureConnected();
2775
+ return new Promise((resolve, reject) => {
2776
+ if (this.pendingSubscription) {
2777
+ clearTimeout(this.pendingSubscription.timeout);
2778
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by new subscription request"));
2775
2779
  }
2776
- } catch (error) {
2777
- console.error("WebSocket subscription error:", error);
2778
- throw new BlinkRealtimeError(
2779
- `Failed to subscribe to channel ${this.channelName}: ${error instanceof Error ? error.message : "Unknown error"}`
2780
- );
2781
- }
2780
+ const timeout = setTimeout(() => {
2781
+ this.pendingSubscription = null;
2782
+ reject(new BlinkRealtimeError("Subscription timeout - no acknowledgment from server"));
2783
+ }, 1e4);
2784
+ this.pendingSubscription = {
2785
+ options,
2786
+ resolve: () => {
2787
+ clearTimeout(timeout);
2788
+ this.pendingSubscription = null;
2789
+ this.isSubscribed = true;
2790
+ this.startHeartbeat();
2791
+ resolve();
2792
+ },
2793
+ reject: (error) => {
2794
+ clearTimeout(timeout);
2795
+ this.pendingSubscription = null;
2796
+ reject(error);
2797
+ },
2798
+ timeout
2799
+ };
2800
+ const subscribeMessage = {
2801
+ type: "subscribe",
2802
+ payload: {
2803
+ channel: this.channelName,
2804
+ userId: options.userId,
2805
+ metadata: options.metadata
2806
+ }
2807
+ };
2808
+ this.websocket.send(JSON.stringify(subscribeMessage));
2809
+ });
2782
2810
  }
2783
2811
  async unsubscribe() {
2784
2812
  if (!this.isSubscribed) {
2785
2813
  return;
2786
2814
  }
2787
- if (this.websocket) {
2815
+ if (this.pendingSubscription) {
2816
+ clearTimeout(this.pendingSubscription.timeout);
2817
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by unsubscribe"));
2818
+ this.pendingSubscription = null;
2819
+ }
2820
+ if (this.websocket && this.websocket.readyState === 1) {
2788
2821
  const unsubscribeMessage = {
2789
2822
  type: "unsubscribe",
2790
2823
  payload: {
@@ -2796,42 +2829,18 @@ var BlinkRealtimeChannel = class {
2796
2829
  this.cleanup();
2797
2830
  }
2798
2831
  async publish(type, data, options = {}) {
2799
- if (!this.websocket || this.websocket.readyState !== 1) {
2800
- throw new BlinkRealtimeError("Not connected to realtime channel. Call subscribe() first.");
2801
- }
2802
- return new Promise((resolve, reject) => {
2803
- const timeout = setTimeout(() => {
2804
- this.websocket.removeEventListener("message", handleResponse);
2805
- reject(new BlinkRealtimeError("Publish timeout - no response from server"));
2806
- }, 5e3);
2807
- const handleResponse = (event) => {
2808
- try {
2809
- const message = JSON.parse(event.data);
2810
- if (message.type === "published" && message.payload.channel === this.channelName) {
2811
- clearTimeout(timeout);
2812
- this.websocket.removeEventListener("message", handleResponse);
2813
- resolve(message.payload.messageId);
2814
- } else if (message.type === "error") {
2815
- clearTimeout(timeout);
2816
- this.websocket.removeEventListener("message", handleResponse);
2817
- reject(new BlinkRealtimeError(`Server error: ${message.payload.error}`));
2818
- }
2819
- } catch (err) {
2820
- }
2821
- };
2822
- this.websocket.addEventListener("message", handleResponse);
2823
- const publishMessage = {
2824
- type: "publish",
2825
- payload: {
2826
- channel: this.channelName,
2827
- type,
2828
- data,
2829
- userId: options.userId,
2830
- metadata: options.metadata
2831
- }
2832
- };
2833
- this.websocket.send(JSON.stringify(publishMessage));
2834
- });
2832
+ await this.ensureConnected();
2833
+ const publishMessage = {
2834
+ type: "publish",
2835
+ payload: {
2836
+ channel: this.channelName,
2837
+ type,
2838
+ data,
2839
+ userId: options.userId,
2840
+ metadata: options.metadata
2841
+ }
2842
+ };
2843
+ return this.sendMessage(JSON.stringify(publishMessage));
2835
2844
  }
2836
2845
  onMessage(callback) {
2837
2846
  this.messageCallbacks.push(callback);
@@ -2878,10 +2887,105 @@ var BlinkRealtimeChannel = class {
2878
2887
  );
2879
2888
  }
2880
2889
  }
2890
+ /**
2891
+ * Ensure WebSocket connection is established and ready
2892
+ */
2893
+ async ensureConnected() {
2894
+ if (this.isConnected && this.websocket?.readyState === 1) {
2895
+ return;
2896
+ }
2897
+ if (this.connectionPromise) {
2898
+ return this.connectionPromise;
2899
+ }
2900
+ this.connectionPromise = this.connectWebSocket();
2901
+ try {
2902
+ await this.connectionPromise;
2903
+ } finally {
2904
+ this.connectionPromise = null;
2905
+ }
2906
+ }
2907
+ /**
2908
+ * Send a message, queuing if socket not ready
2909
+ */
2910
+ sendMessage(message) {
2911
+ return new Promise((resolve, reject) => {
2912
+ const timeout = setTimeout(() => {
2913
+ const index = this.messageQueue.findIndex((q) => q.resolve === resolve);
2914
+ if (index > -1) {
2915
+ this.messageQueue.splice(index, 1);
2916
+ }
2917
+ reject(new BlinkRealtimeError("Message send timeout - no response from server"));
2918
+ }, 1e4);
2919
+ const queuedMessage = {
2920
+ message,
2921
+ resolve,
2922
+ reject,
2923
+ timeout
2924
+ };
2925
+ if (this.websocket && this.websocket.readyState === 1) {
2926
+ this.sendQueuedMessage(queuedMessage);
2927
+ } else {
2928
+ this.messageQueue.push(queuedMessage);
2929
+ }
2930
+ });
2931
+ }
2932
+ /**
2933
+ * Send a queued message and set up response handling
2934
+ */
2935
+ sendQueuedMessage(queuedMessage) {
2936
+ const { message, resolve, reject, timeout } = queuedMessage;
2937
+ const handleResponse = (event) => {
2938
+ try {
2939
+ const response = JSON.parse(event.data);
2940
+ if (response.type === "published" && response.payload.channel === this.channelName) {
2941
+ clearTimeout(timeout);
2942
+ this.websocket.removeEventListener("message", handleResponse);
2943
+ resolve(response.payload.messageId);
2944
+ } else if (response.type === "error") {
2945
+ clearTimeout(timeout);
2946
+ this.websocket.removeEventListener("message", handleResponse);
2947
+ reject(new BlinkRealtimeError(`Server error: ${response.payload.error}`));
2948
+ }
2949
+ } catch (err) {
2950
+ }
2951
+ };
2952
+ this.websocket.addEventListener("message", handleResponse);
2953
+ this.websocket.send(message);
2954
+ }
2955
+ /**
2956
+ * Flush all queued messages when connection becomes ready
2957
+ */
2958
+ flushMessageQueue() {
2959
+ if (!this.websocket || this.websocket.readyState !== 1) {
2960
+ return;
2961
+ }
2962
+ const queue = [...this.messageQueue];
2963
+ this.messageQueue = [];
2964
+ queue.forEach((queuedMessage) => {
2965
+ this.sendQueuedMessage(queuedMessage);
2966
+ });
2967
+ }
2881
2968
  async connectWebSocket() {
2882
2969
  if (this.websocket && this.websocket.readyState === 1) {
2970
+ this.isConnected = true;
2883
2971
  return;
2884
2972
  }
2973
+ if (this.isConnecting) {
2974
+ return new Promise((resolve, reject) => {
2975
+ const checkConnection = () => {
2976
+ if (this.isConnected) {
2977
+ resolve();
2978
+ } else if (!this.isConnecting) {
2979
+ reject(new BlinkRealtimeError("Connection failed"));
2980
+ } else {
2981
+ setTimeout(checkConnection, 100);
2982
+ }
2983
+ };
2984
+ checkConnection();
2985
+ });
2986
+ }
2987
+ this.isConnecting = true;
2988
+ this.isConnected = false;
2885
2989
  return new Promise((resolve, reject) => {
2886
2990
  try {
2887
2991
  const httpClient = this.httpClient;
@@ -2892,12 +2996,16 @@ var BlinkRealtimeChannel = class {
2892
2996
  const WSClass = getWebSocketClass();
2893
2997
  this.websocket = new WSClass(wsUrl);
2894
2998
  if (!this.websocket) {
2999
+ this.isConnecting = false;
2895
3000
  reject(new BlinkRealtimeError("Failed to create WebSocket instance"));
2896
3001
  return;
2897
3002
  }
2898
3003
  this.websocket.onopen = () => {
2899
3004
  console.log(`\u{1F517} Connected to realtime for project ${this.projectId}`);
3005
+ this.isConnecting = false;
3006
+ this.isConnected = true;
2900
3007
  this.reconnectAttempts = 0;
3008
+ this.flushMessageQueue();
2901
3009
  resolve();
2902
3010
  };
2903
3011
  this.websocket.onmessage = (event) => {
@@ -2910,25 +3018,48 @@ var BlinkRealtimeChannel = class {
2910
3018
  };
2911
3019
  this.websocket.onclose = () => {
2912
3020
  console.log(`\u{1F50C} Disconnected from realtime for project ${this.projectId}`);
3021
+ this.isConnecting = false;
3022
+ this.isConnected = false;
2913
3023
  this.isSubscribed = false;
3024
+ this.rejectQueuedMessages(new BlinkRealtimeError("WebSocket connection closed"));
3025
+ if (this.pendingSubscription) {
3026
+ clearTimeout(this.pendingSubscription.timeout);
3027
+ this.pendingSubscription.reject(new BlinkRealtimeError("Connection closed during subscription"));
3028
+ this.pendingSubscription = null;
3029
+ }
2914
3030
  this.scheduleReconnect();
2915
3031
  };
2916
3032
  this.websocket.onerror = (error) => {
2917
3033
  console.error("WebSocket error:", error);
2918
3034
  console.error("WebSocket URL was:", wsUrl);
2919
3035
  console.error("WebSocket readyState:", this.websocket?.readyState);
3036
+ this.isConnecting = false;
3037
+ this.isConnected = false;
2920
3038
  reject(new BlinkRealtimeError(`WebSocket connection failed to ${wsUrl}`));
2921
3039
  };
2922
3040
  setTimeout(() => {
2923
3041
  if (this.websocket?.readyState !== 1) {
3042
+ this.isConnecting = false;
2924
3043
  reject(new BlinkRealtimeError("WebSocket connection timeout"));
2925
3044
  }
2926
- }, 5e3);
3045
+ }, 1e4);
2927
3046
  } catch (error) {
3047
+ this.isConnecting = false;
2928
3048
  reject(new BlinkRealtimeError(`Failed to create WebSocket connection: ${error instanceof Error ? error.message : "Unknown error"}`));
2929
3049
  }
2930
3050
  });
2931
3051
  }
3052
+ /**
3053
+ * Reject all queued messages with the given error
3054
+ */
3055
+ rejectQueuedMessages(error) {
3056
+ const queue = [...this.messageQueue];
3057
+ this.messageQueue = [];
3058
+ queue.forEach((queuedMessage) => {
3059
+ clearTimeout(queuedMessage.timeout);
3060
+ queuedMessage.reject(error);
3061
+ });
3062
+ }
2932
3063
  handleWebSocketMessage(message) {
2933
3064
  switch (message.type) {
2934
3065
  case "message":
@@ -2952,6 +3083,9 @@ var BlinkRealtimeChannel = class {
2952
3083
  break;
2953
3084
  case "subscribed":
2954
3085
  console.log(`\u2705 Subscribed to channel: ${message.payload.channel}`);
3086
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3087
+ this.pendingSubscription.resolve();
3088
+ }
2955
3089
  break;
2956
3090
  case "unsubscribed":
2957
3091
  console.log(`\u274C Unsubscribed from channel: ${message.payload.channel}`);
@@ -2962,6 +3096,9 @@ var BlinkRealtimeChannel = class {
2962
3096
  break;
2963
3097
  case "error":
2964
3098
  console.error("Realtime error:", message.payload.error);
3099
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3100
+ this.pendingSubscription.reject(new BlinkRealtimeError(`Subscription error: ${message.payload.error}`));
3101
+ }
2965
3102
  break;
2966
3103
  default:
2967
3104
  console.log("Unknown message type:", message.type);
@@ -2981,16 +3118,19 @@ var BlinkRealtimeChannel = class {
2981
3118
  if (this.reconnectTimer) {
2982
3119
  clearTimeout(this.reconnectTimer);
2983
3120
  }
3121
+ if (!this.isSubscribed && !this.pendingSubscription) {
3122
+ return;
3123
+ }
2984
3124
  this.reconnectAttempts++;
2985
3125
  const baseDelay = Math.min(3e4, Math.pow(2, this.reconnectAttempts) * 1e3);
2986
3126
  const jitter = Math.random() * 1e3;
2987
3127
  const delay = baseDelay + jitter;
2988
3128
  console.log(`\u{1F504} Scheduling reconnect attempt ${this.reconnectAttempts} in ${Math.round(delay)}ms`);
2989
3129
  this.reconnectTimer = globalThis.setTimeout(async () => {
2990
- if (this.isSubscribed) {
3130
+ if (this.isSubscribed || this.pendingSubscription) {
2991
3131
  try {
2992
3132
  await this.connectWebSocket();
2993
- if (this.websocket) {
3133
+ if (this.isSubscribed && this.websocket) {
2994
3134
  const subscribeMessage = {
2995
3135
  type: "subscribe",
2996
3136
  payload: {
@@ -3009,6 +3149,14 @@ var BlinkRealtimeChannel = class {
3009
3149
  }
3010
3150
  cleanup() {
3011
3151
  this.isSubscribed = false;
3152
+ this.isConnected = false;
3153
+ this.isConnecting = false;
3154
+ if (this.pendingSubscription) {
3155
+ clearTimeout(this.pendingSubscription.timeout);
3156
+ this.pendingSubscription.reject(new BlinkRealtimeError("Channel cleanup"));
3157
+ this.pendingSubscription = null;
3158
+ }
3159
+ this.rejectQueuedMessages(new BlinkRealtimeError("Channel cleanup"));
3012
3160
  if (this.heartbeatTimer) {
3013
3161
  clearInterval(this.heartbeatTimer);
3014
3162
  this.heartbeatTimer = null;
package/dist/index.mjs CHANGED
@@ -2749,40 +2749,73 @@ var BlinkRealtimeChannel = class {
2749
2749
  presenceCallbacks = [];
2750
2750
  websocket = null;
2751
2751
  isSubscribed = false;
2752
+ isConnected = false;
2753
+ isConnecting = false;
2752
2754
  reconnectTimer = null;
2753
2755
  heartbeatTimer = null;
2754
2756
  reconnectAttempts = 0;
2757
+ // Message queuing for when socket is not ready
2758
+ messageQueue = [];
2759
+ pendingSubscription = null;
2760
+ // Connection promise for awaiting readiness
2761
+ connectionPromise = null;
2762
+ /**
2763
+ * Check if channel is ready for publishing
2764
+ */
2765
+ isReady() {
2766
+ return this.isConnected && this.isSubscribed;
2767
+ }
2755
2768
  async subscribe(options = {}) {
2756
2769
  if (this.isSubscribed) {
2757
2770
  return;
2758
2771
  }
2759
- try {
2760
- await this.connectWebSocket();
2761
- if (this.websocket) {
2762
- const subscribeMessage = {
2763
- type: "subscribe",
2764
- payload: {
2765
- channel: this.channelName,
2766
- userId: options.userId,
2767
- metadata: options.metadata
2768
- }
2769
- };
2770
- this.websocket.send(JSON.stringify(subscribeMessage));
2771
- this.isSubscribed = true;
2772
- this.startHeartbeat();
2772
+ await this.ensureConnected();
2773
+ return new Promise((resolve, reject) => {
2774
+ if (this.pendingSubscription) {
2775
+ clearTimeout(this.pendingSubscription.timeout);
2776
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by new subscription request"));
2773
2777
  }
2774
- } catch (error) {
2775
- console.error("WebSocket subscription error:", error);
2776
- throw new BlinkRealtimeError(
2777
- `Failed to subscribe to channel ${this.channelName}: ${error instanceof Error ? error.message : "Unknown error"}`
2778
- );
2779
- }
2778
+ const timeout = setTimeout(() => {
2779
+ this.pendingSubscription = null;
2780
+ reject(new BlinkRealtimeError("Subscription timeout - no acknowledgment from server"));
2781
+ }, 1e4);
2782
+ this.pendingSubscription = {
2783
+ options,
2784
+ resolve: () => {
2785
+ clearTimeout(timeout);
2786
+ this.pendingSubscription = null;
2787
+ this.isSubscribed = true;
2788
+ this.startHeartbeat();
2789
+ resolve();
2790
+ },
2791
+ reject: (error) => {
2792
+ clearTimeout(timeout);
2793
+ this.pendingSubscription = null;
2794
+ reject(error);
2795
+ },
2796
+ timeout
2797
+ };
2798
+ const subscribeMessage = {
2799
+ type: "subscribe",
2800
+ payload: {
2801
+ channel: this.channelName,
2802
+ userId: options.userId,
2803
+ metadata: options.metadata
2804
+ }
2805
+ };
2806
+ this.websocket.send(JSON.stringify(subscribeMessage));
2807
+ });
2780
2808
  }
2781
2809
  async unsubscribe() {
2782
2810
  if (!this.isSubscribed) {
2783
2811
  return;
2784
2812
  }
2785
- if (this.websocket) {
2813
+ if (this.pendingSubscription) {
2814
+ clearTimeout(this.pendingSubscription.timeout);
2815
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by unsubscribe"));
2816
+ this.pendingSubscription = null;
2817
+ }
2818
+ if (this.websocket && this.websocket.readyState === 1) {
2786
2819
  const unsubscribeMessage = {
2787
2820
  type: "unsubscribe",
2788
2821
  payload: {
@@ -2794,42 +2827,18 @@ var BlinkRealtimeChannel = class {
2794
2827
  this.cleanup();
2795
2828
  }
2796
2829
  async publish(type, data, options = {}) {
2797
- if (!this.websocket || this.websocket.readyState !== 1) {
2798
- throw new BlinkRealtimeError("Not connected to realtime channel. Call subscribe() first.");
2799
- }
2800
- return new Promise((resolve, reject) => {
2801
- const timeout = setTimeout(() => {
2802
- this.websocket.removeEventListener("message", handleResponse);
2803
- reject(new BlinkRealtimeError("Publish timeout - no response from server"));
2804
- }, 5e3);
2805
- const handleResponse = (event) => {
2806
- try {
2807
- const message = JSON.parse(event.data);
2808
- if (message.type === "published" && message.payload.channel === this.channelName) {
2809
- clearTimeout(timeout);
2810
- this.websocket.removeEventListener("message", handleResponse);
2811
- resolve(message.payload.messageId);
2812
- } else if (message.type === "error") {
2813
- clearTimeout(timeout);
2814
- this.websocket.removeEventListener("message", handleResponse);
2815
- reject(new BlinkRealtimeError(`Server error: ${message.payload.error}`));
2816
- }
2817
- } catch (err) {
2818
- }
2819
- };
2820
- this.websocket.addEventListener("message", handleResponse);
2821
- const publishMessage = {
2822
- type: "publish",
2823
- payload: {
2824
- channel: this.channelName,
2825
- type,
2826
- data,
2827
- userId: options.userId,
2828
- metadata: options.metadata
2829
- }
2830
- };
2831
- this.websocket.send(JSON.stringify(publishMessage));
2832
- });
2830
+ await this.ensureConnected();
2831
+ const publishMessage = {
2832
+ type: "publish",
2833
+ payload: {
2834
+ channel: this.channelName,
2835
+ type,
2836
+ data,
2837
+ userId: options.userId,
2838
+ metadata: options.metadata
2839
+ }
2840
+ };
2841
+ return this.sendMessage(JSON.stringify(publishMessage));
2833
2842
  }
2834
2843
  onMessage(callback) {
2835
2844
  this.messageCallbacks.push(callback);
@@ -2876,10 +2885,105 @@ var BlinkRealtimeChannel = class {
2876
2885
  );
2877
2886
  }
2878
2887
  }
2888
+ /**
2889
+ * Ensure WebSocket connection is established and ready
2890
+ */
2891
+ async ensureConnected() {
2892
+ if (this.isConnected && this.websocket?.readyState === 1) {
2893
+ return;
2894
+ }
2895
+ if (this.connectionPromise) {
2896
+ return this.connectionPromise;
2897
+ }
2898
+ this.connectionPromise = this.connectWebSocket();
2899
+ try {
2900
+ await this.connectionPromise;
2901
+ } finally {
2902
+ this.connectionPromise = null;
2903
+ }
2904
+ }
2905
+ /**
2906
+ * Send a message, queuing if socket not ready
2907
+ */
2908
+ sendMessage(message) {
2909
+ return new Promise((resolve, reject) => {
2910
+ const timeout = setTimeout(() => {
2911
+ const index = this.messageQueue.findIndex((q) => q.resolve === resolve);
2912
+ if (index > -1) {
2913
+ this.messageQueue.splice(index, 1);
2914
+ }
2915
+ reject(new BlinkRealtimeError("Message send timeout - no response from server"));
2916
+ }, 1e4);
2917
+ const queuedMessage = {
2918
+ message,
2919
+ resolve,
2920
+ reject,
2921
+ timeout
2922
+ };
2923
+ if (this.websocket && this.websocket.readyState === 1) {
2924
+ this.sendQueuedMessage(queuedMessage);
2925
+ } else {
2926
+ this.messageQueue.push(queuedMessage);
2927
+ }
2928
+ });
2929
+ }
2930
+ /**
2931
+ * Send a queued message and set up response handling
2932
+ */
2933
+ sendQueuedMessage(queuedMessage) {
2934
+ const { message, resolve, reject, timeout } = queuedMessage;
2935
+ const handleResponse = (event) => {
2936
+ try {
2937
+ const response = JSON.parse(event.data);
2938
+ if (response.type === "published" && response.payload.channel === this.channelName) {
2939
+ clearTimeout(timeout);
2940
+ this.websocket.removeEventListener("message", handleResponse);
2941
+ resolve(response.payload.messageId);
2942
+ } else if (response.type === "error") {
2943
+ clearTimeout(timeout);
2944
+ this.websocket.removeEventListener("message", handleResponse);
2945
+ reject(new BlinkRealtimeError(`Server error: ${response.payload.error}`));
2946
+ }
2947
+ } catch (err) {
2948
+ }
2949
+ };
2950
+ this.websocket.addEventListener("message", handleResponse);
2951
+ this.websocket.send(message);
2952
+ }
2953
+ /**
2954
+ * Flush all queued messages when connection becomes ready
2955
+ */
2956
+ flushMessageQueue() {
2957
+ if (!this.websocket || this.websocket.readyState !== 1) {
2958
+ return;
2959
+ }
2960
+ const queue = [...this.messageQueue];
2961
+ this.messageQueue = [];
2962
+ queue.forEach((queuedMessage) => {
2963
+ this.sendQueuedMessage(queuedMessage);
2964
+ });
2965
+ }
2879
2966
  async connectWebSocket() {
2880
2967
  if (this.websocket && this.websocket.readyState === 1) {
2968
+ this.isConnected = true;
2881
2969
  return;
2882
2970
  }
2971
+ if (this.isConnecting) {
2972
+ return new Promise((resolve, reject) => {
2973
+ const checkConnection = () => {
2974
+ if (this.isConnected) {
2975
+ resolve();
2976
+ } else if (!this.isConnecting) {
2977
+ reject(new BlinkRealtimeError("Connection failed"));
2978
+ } else {
2979
+ setTimeout(checkConnection, 100);
2980
+ }
2981
+ };
2982
+ checkConnection();
2983
+ });
2984
+ }
2985
+ this.isConnecting = true;
2986
+ this.isConnected = false;
2883
2987
  return new Promise((resolve, reject) => {
2884
2988
  try {
2885
2989
  const httpClient = this.httpClient;
@@ -2890,12 +2994,16 @@ var BlinkRealtimeChannel = class {
2890
2994
  const WSClass = getWebSocketClass();
2891
2995
  this.websocket = new WSClass(wsUrl);
2892
2996
  if (!this.websocket) {
2997
+ this.isConnecting = false;
2893
2998
  reject(new BlinkRealtimeError("Failed to create WebSocket instance"));
2894
2999
  return;
2895
3000
  }
2896
3001
  this.websocket.onopen = () => {
2897
3002
  console.log(`\u{1F517} Connected to realtime for project ${this.projectId}`);
3003
+ this.isConnecting = false;
3004
+ this.isConnected = true;
2898
3005
  this.reconnectAttempts = 0;
3006
+ this.flushMessageQueue();
2899
3007
  resolve();
2900
3008
  };
2901
3009
  this.websocket.onmessage = (event) => {
@@ -2908,25 +3016,48 @@ var BlinkRealtimeChannel = class {
2908
3016
  };
2909
3017
  this.websocket.onclose = () => {
2910
3018
  console.log(`\u{1F50C} Disconnected from realtime for project ${this.projectId}`);
3019
+ this.isConnecting = false;
3020
+ this.isConnected = false;
2911
3021
  this.isSubscribed = false;
3022
+ this.rejectQueuedMessages(new BlinkRealtimeError("WebSocket connection closed"));
3023
+ if (this.pendingSubscription) {
3024
+ clearTimeout(this.pendingSubscription.timeout);
3025
+ this.pendingSubscription.reject(new BlinkRealtimeError("Connection closed during subscription"));
3026
+ this.pendingSubscription = null;
3027
+ }
2912
3028
  this.scheduleReconnect();
2913
3029
  };
2914
3030
  this.websocket.onerror = (error) => {
2915
3031
  console.error("WebSocket error:", error);
2916
3032
  console.error("WebSocket URL was:", wsUrl);
2917
3033
  console.error("WebSocket readyState:", this.websocket?.readyState);
3034
+ this.isConnecting = false;
3035
+ this.isConnected = false;
2918
3036
  reject(new BlinkRealtimeError(`WebSocket connection failed to ${wsUrl}`));
2919
3037
  };
2920
3038
  setTimeout(() => {
2921
3039
  if (this.websocket?.readyState !== 1) {
3040
+ this.isConnecting = false;
2922
3041
  reject(new BlinkRealtimeError("WebSocket connection timeout"));
2923
3042
  }
2924
- }, 5e3);
3043
+ }, 1e4);
2925
3044
  } catch (error) {
3045
+ this.isConnecting = false;
2926
3046
  reject(new BlinkRealtimeError(`Failed to create WebSocket connection: ${error instanceof Error ? error.message : "Unknown error"}`));
2927
3047
  }
2928
3048
  });
2929
3049
  }
3050
+ /**
3051
+ * Reject all queued messages with the given error
3052
+ */
3053
+ rejectQueuedMessages(error) {
3054
+ const queue = [...this.messageQueue];
3055
+ this.messageQueue = [];
3056
+ queue.forEach((queuedMessage) => {
3057
+ clearTimeout(queuedMessage.timeout);
3058
+ queuedMessage.reject(error);
3059
+ });
3060
+ }
2930
3061
  handleWebSocketMessage(message) {
2931
3062
  switch (message.type) {
2932
3063
  case "message":
@@ -2950,6 +3081,9 @@ var BlinkRealtimeChannel = class {
2950
3081
  break;
2951
3082
  case "subscribed":
2952
3083
  console.log(`\u2705 Subscribed to channel: ${message.payload.channel}`);
3084
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3085
+ this.pendingSubscription.resolve();
3086
+ }
2953
3087
  break;
2954
3088
  case "unsubscribed":
2955
3089
  console.log(`\u274C Unsubscribed from channel: ${message.payload.channel}`);
@@ -2960,6 +3094,9 @@ var BlinkRealtimeChannel = class {
2960
3094
  break;
2961
3095
  case "error":
2962
3096
  console.error("Realtime error:", message.payload.error);
3097
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3098
+ this.pendingSubscription.reject(new BlinkRealtimeError(`Subscription error: ${message.payload.error}`));
3099
+ }
2963
3100
  break;
2964
3101
  default:
2965
3102
  console.log("Unknown message type:", message.type);
@@ -2979,16 +3116,19 @@ var BlinkRealtimeChannel = class {
2979
3116
  if (this.reconnectTimer) {
2980
3117
  clearTimeout(this.reconnectTimer);
2981
3118
  }
3119
+ if (!this.isSubscribed && !this.pendingSubscription) {
3120
+ return;
3121
+ }
2982
3122
  this.reconnectAttempts++;
2983
3123
  const baseDelay = Math.min(3e4, Math.pow(2, this.reconnectAttempts) * 1e3);
2984
3124
  const jitter = Math.random() * 1e3;
2985
3125
  const delay = baseDelay + jitter;
2986
3126
  console.log(`\u{1F504} Scheduling reconnect attempt ${this.reconnectAttempts} in ${Math.round(delay)}ms`);
2987
3127
  this.reconnectTimer = globalThis.setTimeout(async () => {
2988
- if (this.isSubscribed) {
3128
+ if (this.isSubscribed || this.pendingSubscription) {
2989
3129
  try {
2990
3130
  await this.connectWebSocket();
2991
- if (this.websocket) {
3131
+ if (this.isSubscribed && this.websocket) {
2992
3132
  const subscribeMessage = {
2993
3133
  type: "subscribe",
2994
3134
  payload: {
@@ -3007,6 +3147,14 @@ var BlinkRealtimeChannel = class {
3007
3147
  }
3008
3148
  cleanup() {
3009
3149
  this.isSubscribed = false;
3150
+ this.isConnected = false;
3151
+ this.isConnecting = false;
3152
+ if (this.pendingSubscription) {
3153
+ clearTimeout(this.pendingSubscription.timeout);
3154
+ this.pendingSubscription.reject(new BlinkRealtimeError("Channel cleanup"));
3155
+ this.pendingSubscription = null;
3156
+ }
3157
+ this.rejectQueuedMessages(new BlinkRealtimeError("Channel cleanup"));
3010
3158
  if (this.heartbeatTimer) {
3011
3159
  clearInterval(this.heartbeatTimer);
3012
3160
  this.heartbeatTimer = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/sdk",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "Blink TypeScript SDK for client-side applications - Zero-boilerplate CRUD + auth + AI + analytics + notifications for modern SaaS/AI apps",
5
5
  "keywords": [
6
6
  "blink",