@blinkdotnew/sdk 0.14.6 → 0.14.8

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
@@ -958,6 +958,8 @@ blink.analytics.clearAttribution()
958
958
  **🎉 Zero-Boilerplate Connection Management!**
959
959
  All connection states, queuing, and reconnection are handled automatically. No more "CONNECTING state" errors!
960
960
 
961
+ **⚠️ React Users**: See the [React + Realtime Connections](#react--realtime-connections) section below for proper async cleanup patterns to avoid "Subscription cancelled" errors.
962
+
961
963
  ```typescript
962
964
  // 🔥 Real-time Messaging & Presence (NEW!)
963
965
  // Perfect for chat apps, live collaboration, multiplayer games, and live updates
@@ -1270,6 +1272,20 @@ All `{{secret_name}}` placeholders are replaced with encrypted values from your
1270
1272
 
1271
1273
  **⚠️ Critical: Avoid Multiple WebSocket Connections**
1272
1274
 
1275
+ The most common mistake is using async functions in useEffect that lose the cleanup function:
1276
+
1277
+ ```typescript
1278
+ // ❌ WRONG - Async function loses cleanup (causes "Subscription cancelled" errors)
1279
+ useEffect(() => {
1280
+ const initApp = async () => {
1281
+ const channel = blink.realtime.channel('room')
1282
+ await channel.subscribe({ userId: user.id })
1283
+ return () => channel.unsubscribe() // ❌ CLEANUP LOST!
1284
+ }
1285
+ initApp() // Returns Promise, not cleanup function
1286
+ }, [])
1287
+ ```
1288
+
1273
1289
  ```typescript
1274
1290
  // ❌ WRONG - Creates new connection on every user change
1275
1291
  useEffect(() => {
@@ -1280,54 +1296,46 @@ useEffect(() => {
1280
1296
  ```
1281
1297
 
1282
1298
  ```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
-
1299
+ // ✅ CORRECT - Proper async cleanup handling
1289
1300
  useEffect(() => {
1290
- if (!user?.id) return
1291
-
1292
- const channel = blink.realtime.channel('room')
1293
- let isMounted = true, isSubscribed = false
1301
+ let channel = null
1294
1302
 
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
- }
1303
+ const initApp = async () => {
1304
+ channel = blink.realtime.channel('room')
1305
+ await channel.subscribe({ userId: user.id })
1313
1306
  }
1314
1307
 
1315
- setup()
1308
+ initApp().catch(console.error)
1309
+
1310
+ // Cleanup runs when component unmounts
1316
1311
  return () => {
1317
- isMounted = false
1318
- setIsConnected(false)
1319
- if (isSubscribed) channel.unsubscribe()
1312
+ channel?.unsubscribe()
1320
1313
  }
1321
1314
  }, [user.id]) // ✅ Only user.id dependency
1315
+ ```
1322
1316
 
1323
- // Publish works automatically - no connection checks needed!
1324
- await blink.realtime.publish('room', 'message', data)
1317
+ ```typescript
1318
+ // ALTERNATIVE - Using state for cleanup
1319
+ const [channel, setChannel] = useState(null)
1320
+
1321
+ useEffect(() => {
1322
+ const initApp = async () => {
1323
+ const ch = blink.realtime.channel('room')
1324
+ await ch.subscribe({ userId: user.id })
1325
+ setChannel(ch)
1326
+ }
1327
+ initApp().catch(console.error)
1328
+ }, [user.id])
1329
+
1330
+ useEffect(() => {
1331
+ return () => channel?.unsubscribe()
1332
+ }, [channel])
1325
1333
  ```
1326
1334
 
1327
1335
  **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
1336
+ 1. **Never return cleanup from async functions** - useEffect cleanup must be synchronous
1337
+ 2. **useEffect dependency**: `[user.id]` not `[user]` to avoid reconnections
1338
+ 3. **Store channel reference** outside async function for cleanup access
1331
1339
  4. **Zero connection management**: SDK handles all connection states automatically
1332
1340
 
1333
1341
  ### React
@@ -1455,10 +1463,11 @@ function RealtimeChat() {
1455
1463
  const [user] = useState({ id: 'user123', name: 'John Doe' }) // From auth
1456
1464
 
1457
1465
  useEffect(() => {
1458
- const channel = blink.realtime.channel('chat-room')
1466
+ let channel = null
1459
1467
 
1460
1468
  // Subscribe and listen for messages
1461
1469
  const setupRealtime = async () => {
1470
+ channel = blink.realtime.channel('chat-room')
1462
1471
  await channel.subscribe({
1463
1472
  userId: user.id,
1464
1473
  metadata: { displayName: user.name, avatar: '/avatar.png' }
@@ -1498,11 +1507,11 @@ function RealtimeChat() {
1498
1507
  })))
1499
1508
  }
1500
1509
 
1501
- setupRealtime()
1510
+ setupRealtime().catch(console.error)
1502
1511
 
1503
1512
  // Cleanup on unmount
1504
1513
  return () => {
1505
- channel.unsubscribe()
1514
+ channel?.unsubscribe()
1506
1515
  }
1507
1516
  }, [user.id, user.name])
1508
1517
 
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
  };
@@ -2805,7 +2811,11 @@ var BlinkRealtimeChannel = class {
2805
2811
  metadata: options.metadata
2806
2812
  }
2807
2813
  };
2808
- this.websocket.send(JSON.stringify(subscribeMessage));
2814
+ this.sendMessage(JSON.stringify(subscribeMessage)).catch((error) => {
2815
+ if (this.pendingSubscription) {
2816
+ this.pendingSubscription.reject(error);
2817
+ }
2818
+ });
2809
2819
  });
2810
2820
  }
2811
2821
  async unsubscribe() {
@@ -2909,6 +2919,13 @@ var BlinkRealtimeChannel = class {
2909
2919
  */
2910
2920
  sendMessage(message) {
2911
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
+ }
2912
2929
  const timeout = setTimeout(() => {
2913
2930
  const index = this.messageQueue.findIndex((q) => q.resolve === resolve);
2914
2931
  if (index > -1) {
@@ -2923,7 +2940,13 @@ var BlinkRealtimeChannel = class {
2923
2940
  timeout
2924
2941
  };
2925
2942
  if (this.websocket && this.websocket.readyState === 1) {
2926
- this.sendQueuedMessage(queuedMessage);
2943
+ if (messageObj.type === "publish") {
2944
+ this.sendQueuedMessage(queuedMessage);
2945
+ } else {
2946
+ this.websocket.send(message);
2947
+ clearTimeout(timeout);
2948
+ resolve("sent");
2949
+ }
2927
2950
  } else {
2928
2951
  this.messageQueue.push(queuedMessage);
2929
2952
  }
@@ -2962,7 +2985,19 @@ var BlinkRealtimeChannel = class {
2962
2985
  const queue = [...this.messageQueue];
2963
2986
  this.messageQueue = [];
2964
2987
  queue.forEach((queuedMessage) => {
2965
- this.sendQueuedMessage(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
+ }
2966
3001
  });
2967
3002
  }
2968
3003
  async connectWebSocket() {
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
  };
@@ -2803,7 +2809,11 @@ var BlinkRealtimeChannel = class {
2803
2809
  metadata: options.metadata
2804
2810
  }
2805
2811
  };
2806
- this.websocket.send(JSON.stringify(subscribeMessage));
2812
+ this.sendMessage(JSON.stringify(subscribeMessage)).catch((error) => {
2813
+ if (this.pendingSubscription) {
2814
+ this.pendingSubscription.reject(error);
2815
+ }
2816
+ });
2807
2817
  });
2808
2818
  }
2809
2819
  async unsubscribe() {
@@ -2907,6 +2917,13 @@ var BlinkRealtimeChannel = class {
2907
2917
  */
2908
2918
  sendMessage(message) {
2909
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
+ }
2910
2927
  const timeout = setTimeout(() => {
2911
2928
  const index = this.messageQueue.findIndex((q) => q.resolve === resolve);
2912
2929
  if (index > -1) {
@@ -2921,7 +2938,13 @@ var BlinkRealtimeChannel = class {
2921
2938
  timeout
2922
2939
  };
2923
2940
  if (this.websocket && this.websocket.readyState === 1) {
2924
- this.sendQueuedMessage(queuedMessage);
2941
+ if (messageObj.type === "publish") {
2942
+ this.sendQueuedMessage(queuedMessage);
2943
+ } else {
2944
+ this.websocket.send(message);
2945
+ clearTimeout(timeout);
2946
+ resolve("sent");
2947
+ }
2925
2948
  } else {
2926
2949
  this.messageQueue.push(queuedMessage);
2927
2950
  }
@@ -2960,7 +2983,19 @@ var BlinkRealtimeChannel = class {
2960
2983
  const queue = [...this.messageQueue];
2961
2984
  this.messageQueue = [];
2962
2985
  queue.forEach((queuedMessage) => {
2963
- this.sendQueuedMessage(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
+ }
2964
2999
  });
2965
3000
  }
2966
3001
  async connectWebSocket() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/sdk",
3
- "version": "0.14.6",
3
+ "version": "0.14.8",
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",