@adventurelabs/scout-core 1.0.35 → 1.0.37
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
|
@@ -54,8 +54,48 @@ export default function ScoutLayout({
|
|
|
54
54
|
|
|
55
55
|
### Hooks
|
|
56
56
|
|
|
57
|
-
- `useScoutDbListener` - Real-time database listening for plans, devices, and tags
|
|
57
|
+
- `useScoutDbListener` - Real-time database listening for plans, devices, and tags with robust disconnect handling
|
|
58
58
|
- `useScoutRefresh` - Data refresh utilities
|
|
59
|
+
- `useConnectionStatus` - Connection status monitoring and manual reconnection controls
|
|
60
|
+
|
|
61
|
+
#### Robust Connection Features
|
|
62
|
+
|
|
63
|
+
The `useScoutDbListener` hook includes several features to handle network disconnections and connection issues:
|
|
64
|
+
|
|
65
|
+
- **Automatic Reconnection**: Automatically attempts to reconnect when the connection is lost
|
|
66
|
+
- **Exponential Backoff**: Uses exponential backoff with jitter to avoid overwhelming the server
|
|
67
|
+
- **Connection State Tracking**: Provides real-time connection status (connected, connecting, disconnected, error)
|
|
68
|
+
- **Error Handling**: Comprehensive error handling with detailed error messages
|
|
69
|
+
- **Manual Reconnection**: Allows manual reconnection attempts via the `reconnect()` function
|
|
70
|
+
- **Retry Limits**: Configurable maximum retry attempts to prevent infinite reconnection loops
|
|
71
|
+
- **Graceful Cleanup**: Proper cleanup of resources when the component unmounts
|
|
72
|
+
|
|
73
|
+
Example usage:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { useConnectionStatus } from "@adventurelabs/scout-core";
|
|
77
|
+
|
|
78
|
+
function ConnectionStatus() {
|
|
79
|
+
const { isConnected, isConnecting, lastError, retryCount, reconnect } =
|
|
80
|
+
useConnectionStatus();
|
|
81
|
+
|
|
82
|
+
if (isConnecting) {
|
|
83
|
+
return <div>Connecting to database...</div>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (lastError) {
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<p>Connection error: {lastError}</p>
|
|
90
|
+
<p>Retry attempts: {retryCount}</p>
|
|
91
|
+
<button onClick={reconnect}>Reconnect</button>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return <div>Status: {isConnected ? "Connected" : "Disconnected"}</div>;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
59
99
|
|
|
60
100
|
### Store
|
|
61
101
|
|
|
@@ -1,3 +1,30 @@
|
|
|
1
1
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import { Database } from "../types/supabase";
|
|
3
|
-
|
|
3
|
+
declare enum ConnectionState {
|
|
4
|
+
DISCONNECTED = "disconnected",
|
|
5
|
+
CONNECTING = "connecting",
|
|
6
|
+
CONNECTED = "connected",
|
|
7
|
+
RECONNECTING = "reconnecting",
|
|
8
|
+
ERROR = "error"
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Hook for listening to real-time database changes with robust disconnect handling.
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Automatic reconnection with exponential backoff
|
|
15
|
+
* - Connection state tracking
|
|
16
|
+
* - Error handling and retry logic
|
|
17
|
+
* - Manual reconnection capability
|
|
18
|
+
*
|
|
19
|
+
* @param scoutSupabase - The Supabase client instance
|
|
20
|
+
* @returns Connection status and control functions
|
|
21
|
+
*/
|
|
22
|
+
export declare function useScoutDbListener(scoutSupabase: SupabaseClient<Database>): {
|
|
23
|
+
connectionState: ConnectionState;
|
|
24
|
+
lastError: string | null;
|
|
25
|
+
retryCount: number;
|
|
26
|
+
reconnect: () => void;
|
|
27
|
+
isConnected: boolean;
|
|
28
|
+
isConnecting: boolean;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -1,188 +1,461 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useAppDispatch } from "../store/hooks";
|
|
3
|
-
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
4
4
|
import { addDevice, addPlan, addTag, addSessionToStore, deleteDevice, deletePlan, deleteSessionFromStore, deleteTag, updateDevice, updatePlan, updateSessionInStore, updateTag, } from "../store/scout";
|
|
5
|
+
// Connection state enum
|
|
6
|
+
var ConnectionState;
|
|
7
|
+
(function (ConnectionState) {
|
|
8
|
+
ConnectionState["DISCONNECTED"] = "disconnected";
|
|
9
|
+
ConnectionState["CONNECTING"] = "connecting";
|
|
10
|
+
ConnectionState["CONNECTED"] = "connected";
|
|
11
|
+
ConnectionState["RECONNECTING"] = "reconnecting";
|
|
12
|
+
ConnectionState["ERROR"] = "error";
|
|
13
|
+
})(ConnectionState || (ConnectionState = {}));
|
|
14
|
+
// Reconnection configuration
|
|
15
|
+
const RECONNECTION_CONFIG = {
|
|
16
|
+
MAX_RETRIES: 10,
|
|
17
|
+
INITIAL_DELAY: 1000, // 1 second
|
|
18
|
+
MAX_DELAY: 30000, // 30 seconds
|
|
19
|
+
BACKOFF_MULTIPLIER: 2,
|
|
20
|
+
JITTER_FACTOR: 0.1, // 10% jitter
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Hook for listening to real-time database changes with robust disconnect handling.
|
|
24
|
+
*
|
|
25
|
+
* Features:
|
|
26
|
+
* - Automatic reconnection with exponential backoff
|
|
27
|
+
* - Connection state tracking
|
|
28
|
+
* - Error handling and retry logic
|
|
29
|
+
* - Manual reconnection capability
|
|
30
|
+
*
|
|
31
|
+
* @param scoutSupabase - The Supabase client instance
|
|
32
|
+
* @returns Connection status and control functions
|
|
33
|
+
*/
|
|
5
34
|
export function useScoutDbListener(scoutSupabase) {
|
|
6
35
|
const supabase = useRef(null);
|
|
7
36
|
const channels = useRef([]);
|
|
8
37
|
const dispatch = useAppDispatch();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
// Connection state management
|
|
39
|
+
const [connectionState, setConnectionState] = useState(ConnectionState.DISCONNECTED);
|
|
40
|
+
const [lastError, setLastError] = useState(null);
|
|
41
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
42
|
+
// Reconnection management
|
|
43
|
+
const reconnectTimeoutRef = useRef(null);
|
|
44
|
+
const isInitializingRef = useRef(false);
|
|
45
|
+
const isDestroyedRef = useRef(false);
|
|
46
|
+
// Calculate exponential backoff delay with jitter
|
|
47
|
+
const calculateBackoffDelay = useCallback((attempt) => {
|
|
48
|
+
const baseDelay = Math.min(RECONNECTION_CONFIG.INITIAL_DELAY *
|
|
49
|
+
Math.pow(RECONNECTION_CONFIG.BACKOFF_MULTIPLIER, attempt), RECONNECTION_CONFIG.MAX_DELAY);
|
|
50
|
+
const jitter = baseDelay * RECONNECTION_CONFIG.JITTER_FACTOR * (Math.random() - 0.5);
|
|
51
|
+
return Math.max(100, baseDelay + jitter); // Minimum 100ms delay
|
|
52
|
+
}, []);
|
|
53
|
+
// Clean up all channels
|
|
54
|
+
const cleanupChannels = useCallback(() => {
|
|
55
|
+
console.log("[DB Listener] 🧹 Cleaning up channels");
|
|
56
|
+
channels.current.forEach((channel) => {
|
|
57
|
+
if (channel && supabase.current) {
|
|
58
|
+
try {
|
|
59
|
+
supabase.current.removeChannel(channel);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.warn("[DB Listener] Error removing channel:", error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
21
65
|
});
|
|
66
|
+
channels.current = [];
|
|
67
|
+
}, []);
|
|
68
|
+
// Cancel any pending reconnection attempts
|
|
69
|
+
const cancelReconnection = useCallback(() => {
|
|
70
|
+
if (reconnectTimeoutRef.current) {
|
|
71
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
72
|
+
reconnectTimeoutRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
// Test database connection
|
|
76
|
+
const testDbConnection = useCallback(async () => {
|
|
77
|
+
if (!supabase.current)
|
|
78
|
+
return false;
|
|
79
|
+
try {
|
|
80
|
+
const { data, error } = await supabase.current
|
|
81
|
+
.from("tags")
|
|
82
|
+
.select("count")
|
|
83
|
+
.limit(1);
|
|
84
|
+
if (error) {
|
|
85
|
+
console.warn("[DB Listener] DB connection test failed:", error);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
console.log("[DB Listener] ✅ DB connection test successful");
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.error("[DB Listener] DB connection test failed:", err);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
// Set up realtime authentication
|
|
97
|
+
const setupRealtimeAuth = useCallback(async () => {
|
|
98
|
+
if (!supabase.current)
|
|
99
|
+
return false;
|
|
100
|
+
try {
|
|
101
|
+
await supabase.current.realtime.setAuth();
|
|
102
|
+
console.log("[DB Listener] ✅ Realtime authentication set up successfully");
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.warn("[DB Listener] ❌ Failed to set up realtime authentication:", err);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}, []);
|
|
110
|
+
// Event handlers
|
|
111
|
+
const handleTagInserts = useCallback((payload) => {
|
|
112
|
+
console.log("[DB Listener] Tag INSERT received:", payload);
|
|
113
|
+
if (!payload.new) {
|
|
114
|
+
console.error("[DB Listener] Tag INSERT - Invalid payload, missing new data");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
dispatch(addTag(payload.new));
|
|
118
|
+
}, [dispatch]);
|
|
119
|
+
const handleTagDeletes = useCallback((payload) => {
|
|
120
|
+
console.log("[DB Listener] Tag DELETE received:", payload);
|
|
22
121
|
if (!payload.old || !payload.old.id) {
|
|
23
122
|
console.error("[DB Listener] Tag DELETE - Invalid payload, missing tag data");
|
|
24
123
|
return;
|
|
25
124
|
}
|
|
26
|
-
console.log("[DB Listener] Tag DELETE - Dispatching deleteTag action with ID:", payload.old.id);
|
|
27
125
|
dispatch(deleteTag(payload.old));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
console.log("[DB Listener] Tag UPDATE received:", payload
|
|
126
|
+
}, [dispatch]);
|
|
127
|
+
const handleTagUpdates = useCallback((payload) => {
|
|
128
|
+
console.log("[DB Listener] Tag UPDATE received:", payload);
|
|
129
|
+
if (!payload.new) {
|
|
130
|
+
console.error("[DB Listener] Tag UPDATE - Invalid payload, missing new data");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
31
133
|
dispatch(updateTag(payload.new));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
console.log("[DB Listener] Device INSERT received:", payload
|
|
35
|
-
|
|
134
|
+
}, [dispatch]);
|
|
135
|
+
const handleDeviceInserts = useCallback((payload) => {
|
|
136
|
+
console.log("[DB Listener] Device INSERT received:", payload);
|
|
137
|
+
if (!payload.new) {
|
|
138
|
+
console.error("[DB Listener] Device INSERT - Invalid payload, missing new data");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
36
141
|
dispatch(addDevice(payload.new));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
console.log("[DB Listener] Device DELETE received:", payload
|
|
142
|
+
}, [dispatch]);
|
|
143
|
+
const handleDeviceDeletes = useCallback((payload) => {
|
|
144
|
+
console.log("[DB Listener] Device DELETE received:", payload);
|
|
145
|
+
if (!payload.old) {
|
|
146
|
+
console.error("[DB Listener] Device DELETE - Invalid payload, missing old data");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
40
149
|
dispatch(deleteDevice(payload.old));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
console.log("[DB Listener] Device UPDATE received:", payload
|
|
44
|
-
|
|
150
|
+
}, [dispatch]);
|
|
151
|
+
const handleDeviceUpdates = useCallback((payload) => {
|
|
152
|
+
console.log("[DB Listener] Device UPDATE received:", payload);
|
|
153
|
+
if (!payload.new) {
|
|
154
|
+
console.error("[DB Listener] Device UPDATE - Invalid payload, missing new data");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
45
157
|
dispatch(updateDevice(payload.new));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log("[DB Listener] Plan INSERT received:", payload
|
|
158
|
+
}, [dispatch]);
|
|
159
|
+
const handlePlanInserts = useCallback((payload) => {
|
|
160
|
+
console.log("[DB Listener] Plan INSERT received:", payload);
|
|
161
|
+
if (!payload.new) {
|
|
162
|
+
console.error("[DB Listener] Plan INSERT - Invalid payload, missing new data");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
49
165
|
dispatch(addPlan(payload.new));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log("[DB Listener] Plan DELETE received:", payload
|
|
166
|
+
}, [dispatch]);
|
|
167
|
+
const handlePlanDeletes = useCallback((payload) => {
|
|
168
|
+
console.log("[DB Listener] Plan DELETE received:", payload);
|
|
169
|
+
if (!payload.old) {
|
|
170
|
+
console.error("[DB Listener] Plan DELETE - Invalid payload, missing old data");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
53
173
|
dispatch(deletePlan(payload.old));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
console.log("[DB Listener] Plan UPDATE received:", payload
|
|
174
|
+
}, [dispatch]);
|
|
175
|
+
const handlePlanUpdates = useCallback((payload) => {
|
|
176
|
+
console.log("[DB Listener] Plan UPDATE received:", payload);
|
|
177
|
+
if (!payload.new) {
|
|
178
|
+
console.error("[DB Listener] Plan UPDATE - Invalid payload, missing new data");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
57
181
|
dispatch(updatePlan(payload.new));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
console.log("[DB Listener] Session INSERT received:", payload
|
|
182
|
+
}, [dispatch]);
|
|
183
|
+
const handleSessionInserts = useCallback((payload) => {
|
|
184
|
+
console.log("[DB Listener] Session INSERT received:", payload);
|
|
185
|
+
if (!payload.new) {
|
|
186
|
+
console.error("[DB Listener] Session INSERT - Invalid payload, missing new data");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
61
189
|
dispatch(addSessionToStore(payload.new));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
console.log("[DB Listener] Session DELETE received:", payload
|
|
190
|
+
}, [dispatch]);
|
|
191
|
+
const handleSessionDeletes = useCallback((payload) => {
|
|
192
|
+
console.log("[DB Listener] Session DELETE received:", payload);
|
|
193
|
+
if (!payload.old) {
|
|
194
|
+
console.error("[DB Listener] Session DELETE - Invalid payload, missing old data");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
65
197
|
dispatch(deleteSessionFromStore(payload.old));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
console.log("[DB Listener] Session UPDATE received:", payload
|
|
198
|
+
}, [dispatch]);
|
|
199
|
+
const handleSessionUpdates = useCallback((payload) => {
|
|
200
|
+
console.log("[DB Listener] Session UPDATE received:", payload);
|
|
201
|
+
if (!payload.new) {
|
|
202
|
+
console.error("[DB Listener] Session UPDATE - Invalid payload, missing new data");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
69
205
|
dispatch(updateSessionInStore(payload.new));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
console.log("[DB Listener] Connectivity INSERT received:", payload
|
|
206
|
+
}, [dispatch]);
|
|
207
|
+
const handleConnectivityInserts = useCallback((payload) => {
|
|
208
|
+
console.log("[DB Listener] Connectivity INSERT received:", payload);
|
|
73
209
|
// For now, we'll just log connectivity changes since they're related to sessions
|
|
74
210
|
// In the future, we might want to update session connectivity data
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
console.log("[DB Listener] Connectivity DELETE received:", payload
|
|
211
|
+
}, []);
|
|
212
|
+
const handleConnectivityDeletes = useCallback((payload) => {
|
|
213
|
+
console.log("[DB Listener] Connectivity DELETE received:", payload);
|
|
78
214
|
// For now, we'll just log connectivity changes since they're related to sessions
|
|
79
215
|
// In the future, we might want to update session connectivity data
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.log("[DB Listener] Connectivity UPDATE received:", payload
|
|
216
|
+
}, []);
|
|
217
|
+
const handleConnectivityUpdates = useCallback((payload) => {
|
|
218
|
+
console.log("[DB Listener] Connectivity UPDATE received:", payload);
|
|
83
219
|
// For now, we'll just log connectivity changes since they're related to sessions
|
|
84
220
|
// In the future, we might want to update session connectivity data
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
221
|
+
}, []);
|
|
222
|
+
// Create a channel with proper error handling
|
|
223
|
+
const createChannel = useCallback((tableName) => {
|
|
224
|
+
if (!supabase.current)
|
|
225
|
+
return null;
|
|
226
|
+
const channelName = `scout_broadcast_${tableName}_${Date.now()}`;
|
|
227
|
+
console.log(`[DB Listener] Creating broadcast channel for ${tableName}:`, channelName);
|
|
228
|
+
try {
|
|
229
|
+
const channel = supabase.current.channel(channelName, {
|
|
230
|
+
config: { private: true },
|
|
231
|
+
});
|
|
232
|
+
// Add system event handlers for connection monitoring
|
|
233
|
+
channel
|
|
234
|
+
.on("system", { event: "disconnect" }, () => {
|
|
235
|
+
console.log(`[DB Listener] 🔌 ${tableName} channel disconnected`);
|
|
236
|
+
if (connectionState === ConnectionState.CONNECTED) {
|
|
237
|
+
setConnectionState(ConnectionState.DISCONNECTED);
|
|
238
|
+
setLastError("Channel disconnected");
|
|
239
|
+
scheduleReconnection();
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
.on("system", { event: "reconnect" }, () => {
|
|
243
|
+
console.log(`[DB Listener] 🔗 ${tableName} channel reconnected`);
|
|
244
|
+
})
|
|
245
|
+
.on("system", { event: "error" }, (error) => {
|
|
246
|
+
console.warn(`[DB Listener] ❌ ${tableName} channel error:`, error);
|
|
247
|
+
setLastError(`Channel error: ${error}`);
|
|
248
|
+
});
|
|
249
|
+
return channel;
|
|
92
250
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error(`[DB Listener] Failed to create ${tableName} channel:`, error);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}, [connectionState]);
|
|
256
|
+
// Set up all channels
|
|
257
|
+
const setupChannels = useCallback(async () => {
|
|
258
|
+
if (!supabase.current)
|
|
259
|
+
return false;
|
|
260
|
+
cleanupChannels();
|
|
261
|
+
const channelConfigs = [
|
|
262
|
+
{
|
|
263
|
+
name: "plans",
|
|
264
|
+
handlers: {
|
|
265
|
+
INSERT: handlePlanInserts,
|
|
266
|
+
UPDATE: handlePlanUpdates,
|
|
267
|
+
DELETE: handlePlanDeletes,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "devices",
|
|
272
|
+
handlers: {
|
|
273
|
+
INSERT: handleDeviceInserts,
|
|
274
|
+
UPDATE: handleDeviceUpdates,
|
|
275
|
+
DELETE: handleDeviceDeletes,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "tags",
|
|
280
|
+
handlers: {
|
|
281
|
+
INSERT: handleTagInserts,
|
|
282
|
+
UPDATE: handleTagUpdates,
|
|
283
|
+
DELETE: handleTagDeletes,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "sessions",
|
|
288
|
+
handlers: {
|
|
289
|
+
INSERT: handleSessionInserts,
|
|
290
|
+
UPDATE: handleSessionUpdates,
|
|
291
|
+
DELETE: handleSessionDeletes,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "connectivity",
|
|
296
|
+
handlers: {
|
|
297
|
+
INSERT: handleConnectivityInserts,
|
|
298
|
+
UPDATE: handleConnectivityUpdates,
|
|
299
|
+
DELETE: handleConnectivityDeletes,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
let successCount = 0;
|
|
304
|
+
const totalChannels = channelConfigs.length;
|
|
305
|
+
for (const config of channelConfigs) {
|
|
306
|
+
const channel = createChannel(config.name);
|
|
307
|
+
if (!channel)
|
|
308
|
+
continue;
|
|
96
309
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
310
|
+
// Set up event handlers
|
|
311
|
+
Object.entries(config.handlers).forEach(([event, handler]) => {
|
|
312
|
+
channel.on("broadcast", { event }, handler);
|
|
313
|
+
});
|
|
314
|
+
// Subscribe to the channel
|
|
315
|
+
const _subscription = channel.subscribe((status) => {
|
|
316
|
+
console.log(`[DB Listener] ${config.name} channel status:`, status);
|
|
317
|
+
if (status === "SUBSCRIBED") {
|
|
318
|
+
successCount++;
|
|
319
|
+
if (successCount === totalChannels) {
|
|
320
|
+
setConnectionState(ConnectionState.CONNECTED);
|
|
321
|
+
setRetryCount(0);
|
|
322
|
+
setLastError(null);
|
|
323
|
+
console.log("[DB Listener] ✅ All channels successfully subscribed");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
327
|
+
console.error(`[DB Listener] ${config.name} channel failed to subscribe:`, status);
|
|
328
|
+
setLastError(`Channel subscription failed: ${status}`);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
channels.current.push(channel);
|
|
100
332
|
}
|
|
101
|
-
catch (
|
|
102
|
-
console.error(
|
|
333
|
+
catch (error) {
|
|
334
|
+
console.error(`[DB Listener] Failed to set up ${config.name} channel:`, error);
|
|
103
335
|
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
336
|
+
}
|
|
337
|
+
return successCount > 0;
|
|
338
|
+
}, [
|
|
339
|
+
cleanupChannels,
|
|
340
|
+
createChannel,
|
|
341
|
+
handlePlanInserts,
|
|
342
|
+
handlePlanUpdates,
|
|
343
|
+
handlePlanDeletes,
|
|
344
|
+
handleDeviceInserts,
|
|
345
|
+
handleDeviceUpdates,
|
|
346
|
+
handleDeviceDeletes,
|
|
347
|
+
handleTagInserts,
|
|
348
|
+
handleTagUpdates,
|
|
349
|
+
handleTagDeletes,
|
|
350
|
+
handleSessionInserts,
|
|
351
|
+
handleSessionUpdates,
|
|
352
|
+
handleSessionDeletes,
|
|
353
|
+
handleConnectivityInserts,
|
|
354
|
+
handleConnectivityUpdates,
|
|
355
|
+
handleConnectivityDeletes,
|
|
356
|
+
]);
|
|
357
|
+
// Schedule reconnection with exponential backoff
|
|
358
|
+
const scheduleReconnection = useCallback(() => {
|
|
359
|
+
if (isDestroyedRef.current ||
|
|
360
|
+
retryCount >= RECONNECTION_CONFIG.MAX_RETRIES) {
|
|
361
|
+
console.log("[DB Listener] Max reconnection attempts reached or hook destroyed");
|
|
362
|
+
setConnectionState(ConnectionState.ERROR);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const delay = calculateBackoffDelay(retryCount);
|
|
366
|
+
console.log(`[DB Listener] Scheduling reconnection attempt ${retryCount + 1} in ${delay}ms`);
|
|
367
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
368
|
+
if (!isDestroyedRef.current) {
|
|
369
|
+
initializeConnection();
|
|
131
370
|
}
|
|
132
|
-
|
|
133
|
-
|
|
371
|
+
}, delay);
|
|
372
|
+
}, [retryCount, calculateBackoffDelay]);
|
|
373
|
+
// Initialize connection
|
|
374
|
+
const initializeConnection = useCallback(async () => {
|
|
375
|
+
if (isDestroyedRef.current || isInitializingRef.current)
|
|
376
|
+
return;
|
|
377
|
+
isInitializingRef.current = true;
|
|
378
|
+
setConnectionState(ConnectionState.CONNECTING);
|
|
379
|
+
try {
|
|
380
|
+
console.log("[DB Listener] 🔄 Initializing connection...");
|
|
381
|
+
// Test database connection
|
|
382
|
+
const dbConnected = await testDbConnection();
|
|
383
|
+
if (!dbConnected) {
|
|
384
|
+
throw new Error("Database connection test failed");
|
|
134
385
|
}
|
|
135
|
-
|
|
136
|
-
|
|
386
|
+
// Set up realtime authentication
|
|
387
|
+
const authSuccess = await setupRealtimeAuth();
|
|
388
|
+
if (!authSuccess) {
|
|
389
|
+
throw new Error("Realtime authentication failed");
|
|
137
390
|
}
|
|
138
|
-
|
|
139
|
-
|
|
391
|
+
// Set up channels
|
|
392
|
+
const channelsSuccess = await setupChannels();
|
|
393
|
+
if (!channelsSuccess) {
|
|
394
|
+
throw new Error("Channel setup failed");
|
|
140
395
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
396
|
+
console.log("[DB Listener] ✅ Connection initialized successfully");
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.error("[DB Listener] ❌ Connection initialization failed:", error);
|
|
400
|
+
setLastError(error instanceof Error ? error.message : "Unknown error");
|
|
401
|
+
setConnectionState(ConnectionState.ERROR);
|
|
402
|
+
setRetryCount((prev) => prev + 1);
|
|
403
|
+
// Schedule reconnection
|
|
404
|
+
scheduleReconnection();
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
isInitializingRef.current = false;
|
|
408
|
+
}
|
|
409
|
+
}, [
|
|
410
|
+
testDbConnection,
|
|
411
|
+
setupRealtimeAuth,
|
|
412
|
+
setupChannels,
|
|
413
|
+
scheduleReconnection,
|
|
414
|
+
]);
|
|
415
|
+
// Manual reconnection function
|
|
416
|
+
const reconnect = useCallback(() => {
|
|
417
|
+
if (isDestroyedRef.current)
|
|
418
|
+
return;
|
|
419
|
+
console.log("[DB Listener] 🔄 Manual reconnection requested");
|
|
420
|
+
cancelReconnection();
|
|
421
|
+
setRetryCount(0);
|
|
422
|
+
setLastError(null);
|
|
423
|
+
initializeConnection();
|
|
424
|
+
}, [cancelReconnection, initializeConnection]);
|
|
425
|
+
// Main effect
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
console.log("=== SCOUT DB LISTENER INITIALIZATION ===");
|
|
428
|
+
if (!scoutSupabase) {
|
|
429
|
+
console.error("[DB Listener] No Supabase client available");
|
|
430
|
+
setConnectionState(ConnectionState.ERROR);
|
|
431
|
+
setLastError("No Supabase client available");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
supabase.current = scoutSupabase;
|
|
435
|
+
isDestroyedRef.current = false;
|
|
436
|
+
// Initialize connection
|
|
437
|
+
initializeConnection();
|
|
177
438
|
// Cleanup function
|
|
178
439
|
return () => {
|
|
179
|
-
console.log("[DB Listener] 🧹 Cleaning up
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
channels.current = [];
|
|
440
|
+
console.log("[DB Listener] 🧹 Cleaning up hook");
|
|
441
|
+
isDestroyedRef.current = true;
|
|
442
|
+
cancelReconnection();
|
|
443
|
+
cleanupChannels();
|
|
186
444
|
};
|
|
187
|
-
}, [
|
|
445
|
+
}, [
|
|
446
|
+
scoutSupabase,
|
|
447
|
+
initializeConnection,
|
|
448
|
+
cancelReconnection,
|
|
449
|
+
cleanupChannels,
|
|
450
|
+
]);
|
|
451
|
+
// Return connection state and manual reconnect function
|
|
452
|
+
return {
|
|
453
|
+
connectionState,
|
|
454
|
+
lastError,
|
|
455
|
+
retryCount,
|
|
456
|
+
reconnect,
|
|
457
|
+
isConnected: connectionState === ConnectionState.CONNECTED,
|
|
458
|
+
isConnecting: connectionState === ConnectionState.CONNECTING ||
|
|
459
|
+
connectionState === ConnectionState.RECONNECTING,
|
|
460
|
+
};
|
|
188
461
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
2
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
3
3
|
import { Database } from "../types/supabase";
|
|
4
|
+
interface ConnectionStatus {
|
|
5
|
+
isConnected: boolean;
|
|
6
|
+
isConnecting: boolean;
|
|
7
|
+
lastError: string | null;
|
|
8
|
+
retryCount: number;
|
|
9
|
+
reconnect: () => void;
|
|
10
|
+
}
|
|
4
11
|
export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
5
12
|
Tables: {
|
|
6
13
|
actions: {
|
|
@@ -883,7 +890,9 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
883
890
|
};
|
|
884
891
|
};
|
|
885
892
|
}>;
|
|
893
|
+
export declare function useConnectionStatus(): ConnectionStatus;
|
|
886
894
|
export interface ScoutRefreshProviderProps {
|
|
887
895
|
children: ReactNode;
|
|
888
896
|
}
|
|
889
897
|
export declare function ScoutRefreshProvider({ children }: ScoutRefreshProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
898
|
+
export {};
|
|
@@ -6,6 +6,7 @@ import { createContext, useContext, useRef } from "react";
|
|
|
6
6
|
import { createBrowserClient } from "@supabase/ssr";
|
|
7
7
|
// Create context for the Supabase client
|
|
8
8
|
const SupabaseContext = createContext(null);
|
|
9
|
+
const ConnectionStatusContext = createContext(null);
|
|
9
10
|
// Hook to use the Supabase client
|
|
10
11
|
export function useSupabase() {
|
|
11
12
|
const supabase = useContext(SupabaseContext);
|
|
@@ -14,6 +15,14 @@ export function useSupabase() {
|
|
|
14
15
|
}
|
|
15
16
|
return supabase;
|
|
16
17
|
}
|
|
18
|
+
// Hook to use connection status
|
|
19
|
+
export function useConnectionStatus() {
|
|
20
|
+
const connectionStatus = useContext(ConnectionStatusContext);
|
|
21
|
+
if (!connectionStatus) {
|
|
22
|
+
throw new Error("useConnectionStatus must be used within a ScoutRefreshProvider");
|
|
23
|
+
}
|
|
24
|
+
return connectionStatus;
|
|
25
|
+
}
|
|
17
26
|
export function ScoutRefreshProvider({ children }) {
|
|
18
27
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
19
28
|
const anon_key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
|
@@ -23,7 +32,15 @@ export function ScoutRefreshProvider({ children }) {
|
|
|
23
32
|
supabaseRef.current = createBrowserClient(url, anon_key);
|
|
24
33
|
console.log("[ScoutRefreshProvider] Created Supabase client");
|
|
25
34
|
}
|
|
26
|
-
|
|
35
|
+
// Use the enhanced DB listener with connection status
|
|
36
|
+
const connectionStatus = useScoutDbListener(supabaseRef.current);
|
|
27
37
|
useScoutRefresh();
|
|
28
|
-
|
|
38
|
+
// Log connection status changes for debugging
|
|
39
|
+
if (connectionStatus.lastError) {
|
|
40
|
+
console.warn("[ScoutRefreshProvider] DB Listener error:", connectionStatus.lastError);
|
|
41
|
+
}
|
|
42
|
+
if (connectionStatus.isConnected) {
|
|
43
|
+
console.log("[ScoutRefreshProvider] ✅ DB Listener connected");
|
|
44
|
+
}
|
|
45
|
+
return (_jsx(SupabaseContext.Provider, { value: supabaseRef.current, children: _jsx(ConnectionStatusContext.Provider, { value: connectionStatus, children: children }) }));
|
|
29
46
|
}
|