@blinkdotnew/sdk 0.14.5 → 0.14.7

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
@@ -1317,17 +1320,15 @@ useEffect(() => {
1317
1320
  }
1318
1321
  }, [user.id]) // ✅ Only user.id dependency
1319
1322
 
1320
- // Only publish when connected
1321
- if (isConnected) {
1322
- await blink.realtime.publish('room', 'message', data)
1323
- }
1323
+ // Publish works automatically - no connection checks needed!
1324
+ await blink.realtime.publish('room', 'message', data)
1324
1325
  ```
1325
1326
 
1326
1327
  **Rules:**
1327
1328
  1. **useEffect dependency**: `[user.id]` not `[user]`
1328
1329
  2. **Use useRef**: Store user data to avoid re-connections
1329
1330
  3. **Add isMounted**: Prevent operations on unmounted components
1330
- 4. **Track connection state**: Only publish when actually connected
1331
+ 4. **Zero connection management**: SDK handles all connection states automatically
1331
1332
 
1332
1333
  ### React
1333
1334
 
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
@@ -1274,7 +1274,13 @@ var BlinkAuth = class {
1274
1274
  */
1275
1275
  onAuthStateChanged(callback) {
1276
1276
  this.listeners.add(callback);
1277
- callback(this.authState);
1277
+ queueMicrotask(() => {
1278
+ try {
1279
+ callback(this.authState);
1280
+ } catch (error) {
1281
+ console.error("Error in auth state change callback:", error);
1282
+ }
1283
+ });
1278
1284
  return () => {
1279
1285
  this.listeners.delete(callback);
1280
1286
  };
@@ -2751,40 +2757,77 @@ var BlinkRealtimeChannel = class {
2751
2757
  presenceCallbacks = [];
2752
2758
  websocket = null;
2753
2759
  isSubscribed = false;
2760
+ isConnected = false;
2761
+ isConnecting = false;
2754
2762
  reconnectTimer = null;
2755
2763
  heartbeatTimer = null;
2756
2764
  reconnectAttempts = 0;
2765
+ // Message queuing for when socket is not ready
2766
+ messageQueue = [];
2767
+ pendingSubscription = null;
2768
+ // Connection promise for awaiting readiness
2769
+ connectionPromise = null;
2770
+ /**
2771
+ * Check if channel is ready for publishing
2772
+ */
2773
+ isReady() {
2774
+ return this.isConnected && this.isSubscribed;
2775
+ }
2757
2776
  async subscribe(options = {}) {
2758
2777
  if (this.isSubscribed) {
2759
2778
  return;
2760
2779
  }
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();
2780
+ await this.ensureConnected();
2781
+ return new Promise((resolve, reject) => {
2782
+ if (this.pendingSubscription) {
2783
+ clearTimeout(this.pendingSubscription.timeout);
2784
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by new subscription request"));
2775
2785
  }
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
- }
2786
+ const timeout = setTimeout(() => {
2787
+ this.pendingSubscription = null;
2788
+ reject(new BlinkRealtimeError("Subscription timeout - no acknowledgment from server"));
2789
+ }, 1e4);
2790
+ this.pendingSubscription = {
2791
+ options,
2792
+ resolve: () => {
2793
+ clearTimeout(timeout);
2794
+ this.pendingSubscription = null;
2795
+ this.isSubscribed = true;
2796
+ this.startHeartbeat();
2797
+ resolve();
2798
+ },
2799
+ reject: (error) => {
2800
+ clearTimeout(timeout);
2801
+ this.pendingSubscription = null;
2802
+ reject(error);
2803
+ },
2804
+ timeout
2805
+ };
2806
+ const subscribeMessage = {
2807
+ type: "subscribe",
2808
+ payload: {
2809
+ channel: this.channelName,
2810
+ userId: options.userId,
2811
+ metadata: options.metadata
2812
+ }
2813
+ };
2814
+ this.sendMessage(JSON.stringify(subscribeMessage)).catch((error) => {
2815
+ if (this.pendingSubscription) {
2816
+ this.pendingSubscription.reject(error);
2817
+ }
2818
+ });
2819
+ });
2782
2820
  }
2783
2821
  async unsubscribe() {
2784
2822
  if (!this.isSubscribed) {
2785
2823
  return;
2786
2824
  }
2787
- if (this.websocket) {
2825
+ if (this.pendingSubscription) {
2826
+ clearTimeout(this.pendingSubscription.timeout);
2827
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by unsubscribe"));
2828
+ this.pendingSubscription = null;
2829
+ }
2830
+ if (this.websocket && this.websocket.readyState === 1) {
2788
2831
  const unsubscribeMessage = {
2789
2832
  type: "unsubscribe",
2790
2833
  payload: {
@@ -2796,42 +2839,18 @@ var BlinkRealtimeChannel = class {
2796
2839
  this.cleanup();
2797
2840
  }
2798
2841
  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
- });
2842
+ await this.ensureConnected();
2843
+ const publishMessage = {
2844
+ type: "publish",
2845
+ payload: {
2846
+ channel: this.channelName,
2847
+ type,
2848
+ data,
2849
+ userId: options.userId,
2850
+ metadata: options.metadata
2851
+ }
2852
+ };
2853
+ return this.sendMessage(JSON.stringify(publishMessage));
2835
2854
  }
2836
2855
  onMessage(callback) {
2837
2856
  this.messageCallbacks.push(callback);
@@ -2878,10 +2897,130 @@ var BlinkRealtimeChannel = class {
2878
2897
  );
2879
2898
  }
2880
2899
  }
2900
+ /**
2901
+ * Ensure WebSocket connection is established and ready
2902
+ */
2903
+ async ensureConnected() {
2904
+ if (this.isConnected && this.websocket?.readyState === 1) {
2905
+ return;
2906
+ }
2907
+ if (this.connectionPromise) {
2908
+ return this.connectionPromise;
2909
+ }
2910
+ this.connectionPromise = this.connectWebSocket();
2911
+ try {
2912
+ await this.connectionPromise;
2913
+ } finally {
2914
+ this.connectionPromise = null;
2915
+ }
2916
+ }
2917
+ /**
2918
+ * Send a message, queuing if socket not ready
2919
+ */
2920
+ sendMessage(message) {
2921
+ return new Promise((resolve, reject) => {
2922
+ let messageObj;
2923
+ try {
2924
+ messageObj = JSON.parse(message);
2925
+ } catch (error) {
2926
+ reject(new BlinkRealtimeError("Invalid message format"));
2927
+ return;
2928
+ }
2929
+ const timeout = setTimeout(() => {
2930
+ const index = this.messageQueue.findIndex((q) => q.resolve === resolve);
2931
+ if (index > -1) {
2932
+ this.messageQueue.splice(index, 1);
2933
+ }
2934
+ reject(new BlinkRealtimeError("Message send timeout - no response from server"));
2935
+ }, 1e4);
2936
+ const queuedMessage = {
2937
+ message,
2938
+ resolve,
2939
+ reject,
2940
+ timeout
2941
+ };
2942
+ if (this.websocket && this.websocket.readyState === 1) {
2943
+ if (messageObj.type === "publish") {
2944
+ this.sendQueuedMessage(queuedMessage);
2945
+ } else {
2946
+ this.websocket.send(message);
2947
+ clearTimeout(timeout);
2948
+ resolve("sent");
2949
+ }
2950
+ } else {
2951
+ this.messageQueue.push(queuedMessage);
2952
+ }
2953
+ });
2954
+ }
2955
+ /**
2956
+ * Send a queued message and set up response handling
2957
+ */
2958
+ sendQueuedMessage(queuedMessage) {
2959
+ const { message, resolve, reject, timeout } = queuedMessage;
2960
+ const handleResponse = (event) => {
2961
+ try {
2962
+ const response = JSON.parse(event.data);
2963
+ if (response.type === "published" && response.payload.channel === this.channelName) {
2964
+ clearTimeout(timeout);
2965
+ this.websocket.removeEventListener("message", handleResponse);
2966
+ resolve(response.payload.messageId);
2967
+ } else if (response.type === "error") {
2968
+ clearTimeout(timeout);
2969
+ this.websocket.removeEventListener("message", handleResponse);
2970
+ reject(new BlinkRealtimeError(`Server error: ${response.payload.error}`));
2971
+ }
2972
+ } catch (err) {
2973
+ }
2974
+ };
2975
+ this.websocket.addEventListener("message", handleResponse);
2976
+ this.websocket.send(message);
2977
+ }
2978
+ /**
2979
+ * Flush all queued messages when connection becomes ready
2980
+ */
2981
+ flushMessageQueue() {
2982
+ if (!this.websocket || this.websocket.readyState !== 1) {
2983
+ return;
2984
+ }
2985
+ const queue = [...this.messageQueue];
2986
+ this.messageQueue = [];
2987
+ queue.forEach((queuedMessage) => {
2988
+ try {
2989
+ const messageObj = JSON.parse(queuedMessage.message);
2990
+ if (messageObj.type === "publish") {
2991
+ this.sendQueuedMessage(queuedMessage);
2992
+ } else {
2993
+ this.websocket.send(queuedMessage.message);
2994
+ clearTimeout(queuedMessage.timeout);
2995
+ queuedMessage.resolve("sent");
2996
+ }
2997
+ } catch (error) {
2998
+ clearTimeout(queuedMessage.timeout);
2999
+ queuedMessage.reject(new BlinkRealtimeError("Invalid queued message format"));
3000
+ }
3001
+ });
3002
+ }
2881
3003
  async connectWebSocket() {
2882
3004
  if (this.websocket && this.websocket.readyState === 1) {
3005
+ this.isConnected = true;
2883
3006
  return;
2884
3007
  }
3008
+ if (this.isConnecting) {
3009
+ return new Promise((resolve, reject) => {
3010
+ const checkConnection = () => {
3011
+ if (this.isConnected) {
3012
+ resolve();
3013
+ } else if (!this.isConnecting) {
3014
+ reject(new BlinkRealtimeError("Connection failed"));
3015
+ } else {
3016
+ setTimeout(checkConnection, 100);
3017
+ }
3018
+ };
3019
+ checkConnection();
3020
+ });
3021
+ }
3022
+ this.isConnecting = true;
3023
+ this.isConnected = false;
2885
3024
  return new Promise((resolve, reject) => {
2886
3025
  try {
2887
3026
  const httpClient = this.httpClient;
@@ -2892,12 +3031,16 @@ var BlinkRealtimeChannel = class {
2892
3031
  const WSClass = getWebSocketClass();
2893
3032
  this.websocket = new WSClass(wsUrl);
2894
3033
  if (!this.websocket) {
3034
+ this.isConnecting = false;
2895
3035
  reject(new BlinkRealtimeError("Failed to create WebSocket instance"));
2896
3036
  return;
2897
3037
  }
2898
3038
  this.websocket.onopen = () => {
2899
3039
  console.log(`\u{1F517} Connected to realtime for project ${this.projectId}`);
3040
+ this.isConnecting = false;
3041
+ this.isConnected = true;
2900
3042
  this.reconnectAttempts = 0;
3043
+ this.flushMessageQueue();
2901
3044
  resolve();
2902
3045
  };
2903
3046
  this.websocket.onmessage = (event) => {
@@ -2910,25 +3053,48 @@ var BlinkRealtimeChannel = class {
2910
3053
  };
2911
3054
  this.websocket.onclose = () => {
2912
3055
  console.log(`\u{1F50C} Disconnected from realtime for project ${this.projectId}`);
3056
+ this.isConnecting = false;
3057
+ this.isConnected = false;
2913
3058
  this.isSubscribed = false;
3059
+ this.rejectQueuedMessages(new BlinkRealtimeError("WebSocket connection closed"));
3060
+ if (this.pendingSubscription) {
3061
+ clearTimeout(this.pendingSubscription.timeout);
3062
+ this.pendingSubscription.reject(new BlinkRealtimeError("Connection closed during subscription"));
3063
+ this.pendingSubscription = null;
3064
+ }
2914
3065
  this.scheduleReconnect();
2915
3066
  };
2916
3067
  this.websocket.onerror = (error) => {
2917
3068
  console.error("WebSocket error:", error);
2918
3069
  console.error("WebSocket URL was:", wsUrl);
2919
3070
  console.error("WebSocket readyState:", this.websocket?.readyState);
3071
+ this.isConnecting = false;
3072
+ this.isConnected = false;
2920
3073
  reject(new BlinkRealtimeError(`WebSocket connection failed to ${wsUrl}`));
2921
3074
  };
2922
3075
  setTimeout(() => {
2923
3076
  if (this.websocket?.readyState !== 1) {
3077
+ this.isConnecting = false;
2924
3078
  reject(new BlinkRealtimeError("WebSocket connection timeout"));
2925
3079
  }
2926
- }, 5e3);
3080
+ }, 1e4);
2927
3081
  } catch (error) {
3082
+ this.isConnecting = false;
2928
3083
  reject(new BlinkRealtimeError(`Failed to create WebSocket connection: ${error instanceof Error ? error.message : "Unknown error"}`));
2929
3084
  }
2930
3085
  });
2931
3086
  }
3087
+ /**
3088
+ * Reject all queued messages with the given error
3089
+ */
3090
+ rejectQueuedMessages(error) {
3091
+ const queue = [...this.messageQueue];
3092
+ this.messageQueue = [];
3093
+ queue.forEach((queuedMessage) => {
3094
+ clearTimeout(queuedMessage.timeout);
3095
+ queuedMessage.reject(error);
3096
+ });
3097
+ }
2932
3098
  handleWebSocketMessage(message) {
2933
3099
  switch (message.type) {
2934
3100
  case "message":
@@ -2952,6 +3118,9 @@ var BlinkRealtimeChannel = class {
2952
3118
  break;
2953
3119
  case "subscribed":
2954
3120
  console.log(`\u2705 Subscribed to channel: ${message.payload.channel}`);
3121
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3122
+ this.pendingSubscription.resolve();
3123
+ }
2955
3124
  break;
2956
3125
  case "unsubscribed":
2957
3126
  console.log(`\u274C Unsubscribed from channel: ${message.payload.channel}`);
@@ -2962,6 +3131,9 @@ var BlinkRealtimeChannel = class {
2962
3131
  break;
2963
3132
  case "error":
2964
3133
  console.error("Realtime error:", message.payload.error);
3134
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3135
+ this.pendingSubscription.reject(new BlinkRealtimeError(`Subscription error: ${message.payload.error}`));
3136
+ }
2965
3137
  break;
2966
3138
  default:
2967
3139
  console.log("Unknown message type:", message.type);
@@ -2981,16 +3153,19 @@ var BlinkRealtimeChannel = class {
2981
3153
  if (this.reconnectTimer) {
2982
3154
  clearTimeout(this.reconnectTimer);
2983
3155
  }
3156
+ if (!this.isSubscribed && !this.pendingSubscription) {
3157
+ return;
3158
+ }
2984
3159
  this.reconnectAttempts++;
2985
3160
  const baseDelay = Math.min(3e4, Math.pow(2, this.reconnectAttempts) * 1e3);
2986
3161
  const jitter = Math.random() * 1e3;
2987
3162
  const delay = baseDelay + jitter;
2988
3163
  console.log(`\u{1F504} Scheduling reconnect attempt ${this.reconnectAttempts} in ${Math.round(delay)}ms`);
2989
3164
  this.reconnectTimer = globalThis.setTimeout(async () => {
2990
- if (this.isSubscribed) {
3165
+ if (this.isSubscribed || this.pendingSubscription) {
2991
3166
  try {
2992
3167
  await this.connectWebSocket();
2993
- if (this.websocket) {
3168
+ if (this.isSubscribed && this.websocket) {
2994
3169
  const subscribeMessage = {
2995
3170
  type: "subscribe",
2996
3171
  payload: {
@@ -3009,6 +3184,14 @@ var BlinkRealtimeChannel = class {
3009
3184
  }
3010
3185
  cleanup() {
3011
3186
  this.isSubscribed = false;
3187
+ this.isConnected = false;
3188
+ this.isConnecting = false;
3189
+ if (this.pendingSubscription) {
3190
+ clearTimeout(this.pendingSubscription.timeout);
3191
+ this.pendingSubscription.reject(new BlinkRealtimeError("Channel cleanup"));
3192
+ this.pendingSubscription = null;
3193
+ }
3194
+ this.rejectQueuedMessages(new BlinkRealtimeError("Channel cleanup"));
3012
3195
  if (this.heartbeatTimer) {
3013
3196
  clearInterval(this.heartbeatTimer);
3014
3197
  this.heartbeatTimer = null;
package/dist/index.mjs CHANGED
@@ -1272,7 +1272,13 @@ var BlinkAuth = class {
1272
1272
  */
1273
1273
  onAuthStateChanged(callback) {
1274
1274
  this.listeners.add(callback);
1275
- callback(this.authState);
1275
+ queueMicrotask(() => {
1276
+ try {
1277
+ callback(this.authState);
1278
+ } catch (error) {
1279
+ console.error("Error in auth state change callback:", error);
1280
+ }
1281
+ });
1276
1282
  return () => {
1277
1283
  this.listeners.delete(callback);
1278
1284
  };
@@ -2749,40 +2755,77 @@ var BlinkRealtimeChannel = class {
2749
2755
  presenceCallbacks = [];
2750
2756
  websocket = null;
2751
2757
  isSubscribed = false;
2758
+ isConnected = false;
2759
+ isConnecting = false;
2752
2760
  reconnectTimer = null;
2753
2761
  heartbeatTimer = null;
2754
2762
  reconnectAttempts = 0;
2763
+ // Message queuing for when socket is not ready
2764
+ messageQueue = [];
2765
+ pendingSubscription = null;
2766
+ // Connection promise for awaiting readiness
2767
+ connectionPromise = null;
2768
+ /**
2769
+ * Check if channel is ready for publishing
2770
+ */
2771
+ isReady() {
2772
+ return this.isConnected && this.isSubscribed;
2773
+ }
2755
2774
  async subscribe(options = {}) {
2756
2775
  if (this.isSubscribed) {
2757
2776
  return;
2758
2777
  }
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();
2778
+ await this.ensureConnected();
2779
+ return new Promise((resolve, reject) => {
2780
+ if (this.pendingSubscription) {
2781
+ clearTimeout(this.pendingSubscription.timeout);
2782
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by new subscription request"));
2773
2783
  }
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
- }
2784
+ const timeout = setTimeout(() => {
2785
+ this.pendingSubscription = null;
2786
+ reject(new BlinkRealtimeError("Subscription timeout - no acknowledgment from server"));
2787
+ }, 1e4);
2788
+ this.pendingSubscription = {
2789
+ options,
2790
+ resolve: () => {
2791
+ clearTimeout(timeout);
2792
+ this.pendingSubscription = null;
2793
+ this.isSubscribed = true;
2794
+ this.startHeartbeat();
2795
+ resolve();
2796
+ },
2797
+ reject: (error) => {
2798
+ clearTimeout(timeout);
2799
+ this.pendingSubscription = null;
2800
+ reject(error);
2801
+ },
2802
+ timeout
2803
+ };
2804
+ const subscribeMessage = {
2805
+ type: "subscribe",
2806
+ payload: {
2807
+ channel: this.channelName,
2808
+ userId: options.userId,
2809
+ metadata: options.metadata
2810
+ }
2811
+ };
2812
+ this.sendMessage(JSON.stringify(subscribeMessage)).catch((error) => {
2813
+ if (this.pendingSubscription) {
2814
+ this.pendingSubscription.reject(error);
2815
+ }
2816
+ });
2817
+ });
2780
2818
  }
2781
2819
  async unsubscribe() {
2782
2820
  if (!this.isSubscribed) {
2783
2821
  return;
2784
2822
  }
2785
- if (this.websocket) {
2823
+ if (this.pendingSubscription) {
2824
+ clearTimeout(this.pendingSubscription.timeout);
2825
+ this.pendingSubscription.reject(new BlinkRealtimeError("Subscription cancelled by unsubscribe"));
2826
+ this.pendingSubscription = null;
2827
+ }
2828
+ if (this.websocket && this.websocket.readyState === 1) {
2786
2829
  const unsubscribeMessage = {
2787
2830
  type: "unsubscribe",
2788
2831
  payload: {
@@ -2794,42 +2837,18 @@ var BlinkRealtimeChannel = class {
2794
2837
  this.cleanup();
2795
2838
  }
2796
2839
  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
- });
2840
+ await this.ensureConnected();
2841
+ const publishMessage = {
2842
+ type: "publish",
2843
+ payload: {
2844
+ channel: this.channelName,
2845
+ type,
2846
+ data,
2847
+ userId: options.userId,
2848
+ metadata: options.metadata
2849
+ }
2850
+ };
2851
+ return this.sendMessage(JSON.stringify(publishMessage));
2833
2852
  }
2834
2853
  onMessage(callback) {
2835
2854
  this.messageCallbacks.push(callback);
@@ -2876,10 +2895,130 @@ var BlinkRealtimeChannel = class {
2876
2895
  );
2877
2896
  }
2878
2897
  }
2898
+ /**
2899
+ * Ensure WebSocket connection is established and ready
2900
+ */
2901
+ async ensureConnected() {
2902
+ if (this.isConnected && this.websocket?.readyState === 1) {
2903
+ return;
2904
+ }
2905
+ if (this.connectionPromise) {
2906
+ return this.connectionPromise;
2907
+ }
2908
+ this.connectionPromise = this.connectWebSocket();
2909
+ try {
2910
+ await this.connectionPromise;
2911
+ } finally {
2912
+ this.connectionPromise = null;
2913
+ }
2914
+ }
2915
+ /**
2916
+ * Send a message, queuing if socket not ready
2917
+ */
2918
+ sendMessage(message) {
2919
+ return new Promise((resolve, reject) => {
2920
+ let messageObj;
2921
+ try {
2922
+ messageObj = JSON.parse(message);
2923
+ } catch (error) {
2924
+ reject(new BlinkRealtimeError("Invalid message format"));
2925
+ return;
2926
+ }
2927
+ const timeout = setTimeout(() => {
2928
+ const index = this.messageQueue.findIndex((q) => q.resolve === resolve);
2929
+ if (index > -1) {
2930
+ this.messageQueue.splice(index, 1);
2931
+ }
2932
+ reject(new BlinkRealtimeError("Message send timeout - no response from server"));
2933
+ }, 1e4);
2934
+ const queuedMessage = {
2935
+ message,
2936
+ resolve,
2937
+ reject,
2938
+ timeout
2939
+ };
2940
+ if (this.websocket && this.websocket.readyState === 1) {
2941
+ if (messageObj.type === "publish") {
2942
+ this.sendQueuedMessage(queuedMessage);
2943
+ } else {
2944
+ this.websocket.send(message);
2945
+ clearTimeout(timeout);
2946
+ resolve("sent");
2947
+ }
2948
+ } else {
2949
+ this.messageQueue.push(queuedMessage);
2950
+ }
2951
+ });
2952
+ }
2953
+ /**
2954
+ * Send a queued message and set up response handling
2955
+ */
2956
+ sendQueuedMessage(queuedMessage) {
2957
+ const { message, resolve, reject, timeout } = queuedMessage;
2958
+ const handleResponse = (event) => {
2959
+ try {
2960
+ const response = JSON.parse(event.data);
2961
+ if (response.type === "published" && response.payload.channel === this.channelName) {
2962
+ clearTimeout(timeout);
2963
+ this.websocket.removeEventListener("message", handleResponse);
2964
+ resolve(response.payload.messageId);
2965
+ } else if (response.type === "error") {
2966
+ clearTimeout(timeout);
2967
+ this.websocket.removeEventListener("message", handleResponse);
2968
+ reject(new BlinkRealtimeError(`Server error: ${response.payload.error}`));
2969
+ }
2970
+ } catch (err) {
2971
+ }
2972
+ };
2973
+ this.websocket.addEventListener("message", handleResponse);
2974
+ this.websocket.send(message);
2975
+ }
2976
+ /**
2977
+ * Flush all queued messages when connection becomes ready
2978
+ */
2979
+ flushMessageQueue() {
2980
+ if (!this.websocket || this.websocket.readyState !== 1) {
2981
+ return;
2982
+ }
2983
+ const queue = [...this.messageQueue];
2984
+ this.messageQueue = [];
2985
+ queue.forEach((queuedMessage) => {
2986
+ try {
2987
+ const messageObj = JSON.parse(queuedMessage.message);
2988
+ if (messageObj.type === "publish") {
2989
+ this.sendQueuedMessage(queuedMessage);
2990
+ } else {
2991
+ this.websocket.send(queuedMessage.message);
2992
+ clearTimeout(queuedMessage.timeout);
2993
+ queuedMessage.resolve("sent");
2994
+ }
2995
+ } catch (error) {
2996
+ clearTimeout(queuedMessage.timeout);
2997
+ queuedMessage.reject(new BlinkRealtimeError("Invalid queued message format"));
2998
+ }
2999
+ });
3000
+ }
2879
3001
  async connectWebSocket() {
2880
3002
  if (this.websocket && this.websocket.readyState === 1) {
3003
+ this.isConnected = true;
2881
3004
  return;
2882
3005
  }
3006
+ if (this.isConnecting) {
3007
+ return new Promise((resolve, reject) => {
3008
+ const checkConnection = () => {
3009
+ if (this.isConnected) {
3010
+ resolve();
3011
+ } else if (!this.isConnecting) {
3012
+ reject(new BlinkRealtimeError("Connection failed"));
3013
+ } else {
3014
+ setTimeout(checkConnection, 100);
3015
+ }
3016
+ };
3017
+ checkConnection();
3018
+ });
3019
+ }
3020
+ this.isConnecting = true;
3021
+ this.isConnected = false;
2883
3022
  return new Promise((resolve, reject) => {
2884
3023
  try {
2885
3024
  const httpClient = this.httpClient;
@@ -2890,12 +3029,16 @@ var BlinkRealtimeChannel = class {
2890
3029
  const WSClass = getWebSocketClass();
2891
3030
  this.websocket = new WSClass(wsUrl);
2892
3031
  if (!this.websocket) {
3032
+ this.isConnecting = false;
2893
3033
  reject(new BlinkRealtimeError("Failed to create WebSocket instance"));
2894
3034
  return;
2895
3035
  }
2896
3036
  this.websocket.onopen = () => {
2897
3037
  console.log(`\u{1F517} Connected to realtime for project ${this.projectId}`);
3038
+ this.isConnecting = false;
3039
+ this.isConnected = true;
2898
3040
  this.reconnectAttempts = 0;
3041
+ this.flushMessageQueue();
2899
3042
  resolve();
2900
3043
  };
2901
3044
  this.websocket.onmessage = (event) => {
@@ -2908,25 +3051,48 @@ var BlinkRealtimeChannel = class {
2908
3051
  };
2909
3052
  this.websocket.onclose = () => {
2910
3053
  console.log(`\u{1F50C} Disconnected from realtime for project ${this.projectId}`);
3054
+ this.isConnecting = false;
3055
+ this.isConnected = false;
2911
3056
  this.isSubscribed = false;
3057
+ this.rejectQueuedMessages(new BlinkRealtimeError("WebSocket connection closed"));
3058
+ if (this.pendingSubscription) {
3059
+ clearTimeout(this.pendingSubscription.timeout);
3060
+ this.pendingSubscription.reject(new BlinkRealtimeError("Connection closed during subscription"));
3061
+ this.pendingSubscription = null;
3062
+ }
2912
3063
  this.scheduleReconnect();
2913
3064
  };
2914
3065
  this.websocket.onerror = (error) => {
2915
3066
  console.error("WebSocket error:", error);
2916
3067
  console.error("WebSocket URL was:", wsUrl);
2917
3068
  console.error("WebSocket readyState:", this.websocket?.readyState);
3069
+ this.isConnecting = false;
3070
+ this.isConnected = false;
2918
3071
  reject(new BlinkRealtimeError(`WebSocket connection failed to ${wsUrl}`));
2919
3072
  };
2920
3073
  setTimeout(() => {
2921
3074
  if (this.websocket?.readyState !== 1) {
3075
+ this.isConnecting = false;
2922
3076
  reject(new BlinkRealtimeError("WebSocket connection timeout"));
2923
3077
  }
2924
- }, 5e3);
3078
+ }, 1e4);
2925
3079
  } catch (error) {
3080
+ this.isConnecting = false;
2926
3081
  reject(new BlinkRealtimeError(`Failed to create WebSocket connection: ${error instanceof Error ? error.message : "Unknown error"}`));
2927
3082
  }
2928
3083
  });
2929
3084
  }
3085
+ /**
3086
+ * Reject all queued messages with the given error
3087
+ */
3088
+ rejectQueuedMessages(error) {
3089
+ const queue = [...this.messageQueue];
3090
+ this.messageQueue = [];
3091
+ queue.forEach((queuedMessage) => {
3092
+ clearTimeout(queuedMessage.timeout);
3093
+ queuedMessage.reject(error);
3094
+ });
3095
+ }
2930
3096
  handleWebSocketMessage(message) {
2931
3097
  switch (message.type) {
2932
3098
  case "message":
@@ -2950,6 +3116,9 @@ var BlinkRealtimeChannel = class {
2950
3116
  break;
2951
3117
  case "subscribed":
2952
3118
  console.log(`\u2705 Subscribed to channel: ${message.payload.channel}`);
3119
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3120
+ this.pendingSubscription.resolve();
3121
+ }
2953
3122
  break;
2954
3123
  case "unsubscribed":
2955
3124
  console.log(`\u274C Unsubscribed from channel: ${message.payload.channel}`);
@@ -2960,6 +3129,9 @@ var BlinkRealtimeChannel = class {
2960
3129
  break;
2961
3130
  case "error":
2962
3131
  console.error("Realtime error:", message.payload.error);
3132
+ if (this.pendingSubscription && message.payload.channel === this.channelName) {
3133
+ this.pendingSubscription.reject(new BlinkRealtimeError(`Subscription error: ${message.payload.error}`));
3134
+ }
2963
3135
  break;
2964
3136
  default:
2965
3137
  console.log("Unknown message type:", message.type);
@@ -2979,16 +3151,19 @@ var BlinkRealtimeChannel = class {
2979
3151
  if (this.reconnectTimer) {
2980
3152
  clearTimeout(this.reconnectTimer);
2981
3153
  }
3154
+ if (!this.isSubscribed && !this.pendingSubscription) {
3155
+ return;
3156
+ }
2982
3157
  this.reconnectAttempts++;
2983
3158
  const baseDelay = Math.min(3e4, Math.pow(2, this.reconnectAttempts) * 1e3);
2984
3159
  const jitter = Math.random() * 1e3;
2985
3160
  const delay = baseDelay + jitter;
2986
3161
  console.log(`\u{1F504} Scheduling reconnect attempt ${this.reconnectAttempts} in ${Math.round(delay)}ms`);
2987
3162
  this.reconnectTimer = globalThis.setTimeout(async () => {
2988
- if (this.isSubscribed) {
3163
+ if (this.isSubscribed || this.pendingSubscription) {
2989
3164
  try {
2990
3165
  await this.connectWebSocket();
2991
- if (this.websocket) {
3166
+ if (this.isSubscribed && this.websocket) {
2992
3167
  const subscribeMessage = {
2993
3168
  type: "subscribe",
2994
3169
  payload: {
@@ -3007,6 +3182,14 @@ var BlinkRealtimeChannel = class {
3007
3182
  }
3008
3183
  cleanup() {
3009
3184
  this.isSubscribed = false;
3185
+ this.isConnected = false;
3186
+ this.isConnecting = false;
3187
+ if (this.pendingSubscription) {
3188
+ clearTimeout(this.pendingSubscription.timeout);
3189
+ this.pendingSubscription.reject(new BlinkRealtimeError("Channel cleanup"));
3190
+ this.pendingSubscription = null;
3191
+ }
3192
+ this.rejectQueuedMessages(new BlinkRealtimeError("Channel cleanup"));
3010
3193
  if (this.heartbeatTimer) {
3011
3194
  clearInterval(this.heartbeatTimer);
3012
3195
  this.heartbeatTimer = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/sdk",
3
- "version": "0.14.5",
3
+ "version": "0.14.7",
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",