@adventurelabs/scout-core 1.0.36 โ 1.0.38
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,65 @@
|
|
|
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
|
+
ERROR = "error"
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Hook for listening to real-time database changes with robust disconnect handling.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Automatic reconnection with exponential backoff
|
|
14
|
+
* - Connection state tracking
|
|
15
|
+
* - Error handling and retry logic
|
|
16
|
+
* - Manual reconnection capability
|
|
17
|
+
*
|
|
18
|
+
* @param scoutSupabase - The Supabase client instance
|
|
19
|
+
* @returns Connection status and control functions
|
|
20
|
+
*/
|
|
21
|
+
export declare function useScoutDbListener(scoutSupabase: SupabaseClient<Database>): {
|
|
22
|
+
connectionState: ConnectionState;
|
|
23
|
+
lastError: string | null;
|
|
24
|
+
retryCount: number;
|
|
25
|
+
reconnect: () => void;
|
|
26
|
+
isConnected: boolean;
|
|
27
|
+
isConnecting: boolean;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
30
|
+
/**
|
|
31
|
+
* Return type for useScoutDbListener hook
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* function MyComponent() {
|
|
36
|
+
* const {
|
|
37
|
+
* isConnected,
|
|
38
|
+
* isConnecting,
|
|
39
|
+
* lastError,
|
|
40
|
+
* retryCount,
|
|
41
|
+
* reconnect
|
|
42
|
+
* } = useConnectionStatus();
|
|
43
|
+
*
|
|
44
|
+
* if (isConnecting) {
|
|
45
|
+
* return <div>Connecting to database...</div>;
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* if (lastError) {
|
|
49
|
+
* return (
|
|
50
|
+
* <div>
|
|
51
|
+
* <p>Connection error: {lastError}</p>
|
|
52
|
+
* <p>Retry attempts: {retryCount}</p>
|
|
53
|
+
* <button onClick={reconnect}>Reconnect</button>
|
|
54
|
+
* </div>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* if (!isConnected) {
|
|
59
|
+
* return <div>Disconnected from database</div>;
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* return <div>Connected to database</div>;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
@@ -1,284 +1,327 @@
|
|
|
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["ERROR"] = "error";
|
|
12
|
+
})(ConnectionState || (ConnectionState = {}));
|
|
13
|
+
// Reconnection configuration
|
|
14
|
+
const RECONNECTION_CONFIG = {
|
|
15
|
+
MAX_RETRIES: 10,
|
|
16
|
+
INITIAL_DELAY: 1000, // 1 second
|
|
17
|
+
MAX_DELAY: 30000, // 30 seconds
|
|
18
|
+
BACKOFF_MULTIPLIER: 2,
|
|
19
|
+
JITTER_FACTOR: 0.1, // 10% jitter
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Hook for listening to real-time database changes with robust disconnect handling.
|
|
23
|
+
*
|
|
24
|
+
* Features:
|
|
25
|
+
* - Automatic reconnection with exponential backoff
|
|
26
|
+
* - Connection state tracking
|
|
27
|
+
* - Error handling and retry logic
|
|
28
|
+
* - Manual reconnection capability
|
|
29
|
+
*
|
|
30
|
+
* @param scoutSupabase - The Supabase client instance
|
|
31
|
+
* @returns Connection status and control functions
|
|
32
|
+
*/
|
|
5
33
|
export function useScoutDbListener(scoutSupabase) {
|
|
6
34
|
const supabase = useRef(null);
|
|
7
35
|
const channels = useRef([]);
|
|
8
36
|
const dispatch = useAppDispatch();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
// Connection state management
|
|
38
|
+
const [connectionState, setConnectionState] = useState(ConnectionState.DISCONNECTED);
|
|
39
|
+
const [lastError, setLastError] = useState(null);
|
|
40
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
41
|
+
// Reconnection management
|
|
42
|
+
const reconnectTimeoutRef = useRef(null);
|
|
43
|
+
const isInitializingRef = useRef(false);
|
|
44
|
+
const isDestroyedRef = useRef(false);
|
|
45
|
+
// Calculate exponential backoff delay with jitter
|
|
46
|
+
const calculateBackoffDelay = useCallback((attempt) => {
|
|
47
|
+
const baseDelay = Math.min(RECONNECTION_CONFIG.INITIAL_DELAY *
|
|
48
|
+
Math.pow(RECONNECTION_CONFIG.BACKOFF_MULTIPLIER, attempt), RECONNECTION_CONFIG.MAX_DELAY);
|
|
49
|
+
const jitter = baseDelay * RECONNECTION_CONFIG.JITTER_FACTOR * (Math.random() - 0.5);
|
|
50
|
+
return Math.max(100, baseDelay + jitter); // Minimum 100ms delay
|
|
51
|
+
}, []);
|
|
52
|
+
// Clean up all channels
|
|
53
|
+
const cleanupChannels = useCallback(() => {
|
|
54
|
+
console.log("[DB Listener] ๐งน Cleaning up channels");
|
|
55
|
+
channels.current.forEach((channel) => {
|
|
56
|
+
if (channel && supabase.current) {
|
|
57
|
+
try {
|
|
58
|
+
supabase.current.removeChannel(channel);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.warn("[DB Listener] Error removing channel:", error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
23
64
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
dispatch(deleteTag(tagData));
|
|
32
|
-
}
|
|
33
|
-
function handleTagUpdates(payload) {
|
|
34
|
-
console.log("[DB Listener] Tag UPDATE received:", payload);
|
|
35
|
-
// Broadcast payload contains the new record
|
|
36
|
-
const tagData = payload.new || payload;
|
|
37
|
-
dispatch(updateTag(tagData));
|
|
38
|
-
}
|
|
39
|
-
async function handleDeviceInserts(payload) {
|
|
40
|
-
console.log("[DB Listener] Device INSERT received:", payload);
|
|
41
|
-
// Broadcast payload contains the record directly
|
|
42
|
-
const deviceData = payload.new || payload;
|
|
43
|
-
dispatch(addDevice(deviceData));
|
|
44
|
-
}
|
|
45
|
-
function handleDeviceDeletes(payload) {
|
|
46
|
-
console.log("[DB Listener] Device DELETE received:", payload);
|
|
47
|
-
// Broadcast payload contains the old record
|
|
48
|
-
const deviceData = payload.old || payload;
|
|
49
|
-
dispatch(deleteDevice(deviceData));
|
|
50
|
-
}
|
|
51
|
-
async function handleDeviceUpdates(payload) {
|
|
52
|
-
console.log("[DB Listener] Device UPDATE received:", payload);
|
|
53
|
-
// Broadcast payload contains the new record
|
|
54
|
-
const deviceData = payload.new || payload;
|
|
55
|
-
dispatch(updateDevice(deviceData));
|
|
56
|
-
}
|
|
57
|
-
function handlePlanInserts(payload) {
|
|
58
|
-
console.log("[DB Listener] Plan INSERT received:", payload);
|
|
59
|
-
// Broadcast payload contains the record directly
|
|
60
|
-
const planData = payload.new || payload;
|
|
61
|
-
dispatch(addPlan(planData));
|
|
62
|
-
}
|
|
63
|
-
function handlePlanDeletes(payload) {
|
|
64
|
-
console.log("[DB Listener] Plan DELETE received:", payload);
|
|
65
|
-
// Broadcast payload contains the old record
|
|
66
|
-
const planData = payload.old || payload;
|
|
67
|
-
dispatch(deletePlan(planData));
|
|
68
|
-
}
|
|
69
|
-
function handlePlanUpdates(payload) {
|
|
70
|
-
console.log("[DB Listener] Plan UPDATE received:", payload);
|
|
71
|
-
// Broadcast payload contains the new record
|
|
72
|
-
const planData = payload.new || payload;
|
|
73
|
-
dispatch(updatePlan(planData));
|
|
74
|
-
}
|
|
75
|
-
function handleSessionInserts(payload) {
|
|
76
|
-
console.log("[DB Listener] Session INSERT received:", payload);
|
|
77
|
-
// Broadcast payload contains the record directly
|
|
78
|
-
const sessionData = payload.new || payload;
|
|
79
|
-
dispatch(addSessionToStore(sessionData));
|
|
80
|
-
}
|
|
81
|
-
function handleSessionDeletes(payload) {
|
|
82
|
-
console.log("[DB Listener] Session DELETE received:", payload);
|
|
83
|
-
// Broadcast payload contains the old record
|
|
84
|
-
const sessionData = payload.old || payload;
|
|
85
|
-
dispatch(deleteSessionFromStore(sessionData));
|
|
86
|
-
}
|
|
87
|
-
function handleSessionUpdates(payload) {
|
|
88
|
-
console.log("[DB Listener] Session UPDATE received:", payload);
|
|
89
|
-
// Broadcast payload contains the new record
|
|
90
|
-
const sessionData = payload.new || payload;
|
|
91
|
-
dispatch(updateSessionInStore(sessionData));
|
|
92
|
-
}
|
|
93
|
-
function handleConnectivityInserts(payload) {
|
|
94
|
-
console.log("[DB Listener] Connectivity INSERT received:", payload);
|
|
95
|
-
// For now, we'll just log connectivity changes since they're related to sessions
|
|
96
|
-
// In the future, we might want to update session connectivity data
|
|
97
|
-
}
|
|
98
|
-
function handleConnectivityDeletes(payload) {
|
|
99
|
-
console.log("[DB Listener] Connectivity DELETE received:", payload);
|
|
100
|
-
// For now, we'll just log connectivity changes since they're related to sessions
|
|
101
|
-
// In the future, we might want to update session connectivity data
|
|
102
|
-
}
|
|
103
|
-
function handleConnectivityUpdates(payload) {
|
|
104
|
-
console.log("[DB Listener] Connectivity UPDATE received:", payload);
|
|
105
|
-
// For now, we'll just log connectivity changes since they're related to sessions
|
|
106
|
-
// In the future, we might want to update session connectivity data
|
|
107
|
-
}
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
console.log("=== SCOUT DB LISTENER DEBUG ===");
|
|
110
|
-
console.log("[DB Listener] Using shared Supabase client from ScoutRefreshProvider context");
|
|
111
|
-
if (!scoutSupabase) {
|
|
112
|
-
console.error("[DB Listener] No Supabase client available from ScoutRefreshProvider context");
|
|
113
|
-
return;
|
|
65
|
+
channels.current = [];
|
|
66
|
+
}, []);
|
|
67
|
+
// Cancel any pending reconnection attempts
|
|
68
|
+
const cancelReconnection = useCallback(() => {
|
|
69
|
+
if (reconnectTimeoutRef.current) {
|
|
70
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
71
|
+
reconnectTimeoutRef.current = null;
|
|
114
72
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
73
|
+
}, []);
|
|
74
|
+
// Test database connection
|
|
75
|
+
const testDbConnection = useCallback(async () => {
|
|
76
|
+
if (!supabase.current)
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
const { data, error } = await supabase.current
|
|
80
|
+
.from("tags")
|
|
81
|
+
.select("count")
|
|
82
|
+
.limit(1);
|
|
83
|
+
if (error) {
|
|
84
|
+
console.warn("[DB Listener] DB connection test failed:", error);
|
|
85
|
+
return false;
|
|
122
86
|
}
|
|
123
|
-
|
|
124
|
-
|
|
87
|
+
console.log("[DB Listener] โ
DB connection test successful");
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error("[DB Listener] DB connection test failed:", err);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
// Set up realtime authentication
|
|
96
|
+
const setupRealtimeAuth = useCallback(async () => {
|
|
97
|
+
if (!supabase.current)
|
|
98
|
+
return false;
|
|
99
|
+
try {
|
|
100
|
+
await supabase.current.realtime.setAuth();
|
|
101
|
+
console.log("[DB Listener] โ
Realtime authentication set up successfully");
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.warn("[DB Listener] โ Failed to set up realtime authentication:", err);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}, []);
|
|
109
|
+
// Generic event handler factory
|
|
110
|
+
const createEventHandler = useCallback((action, dataKey, entityName) => {
|
|
111
|
+
return (payload) => {
|
|
112
|
+
console.log(`[DB Listener] ${entityName} ${payload.event} received:`, payload);
|
|
113
|
+
const data = payload[dataKey];
|
|
114
|
+
if (!data) {
|
|
115
|
+
console.error(`[DB Listener] ${entityName} ${payload.event} - Invalid payload, missing ${dataKey} data`);
|
|
116
|
+
return;
|
|
125
117
|
}
|
|
118
|
+
action(data);
|
|
126
119
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
}, []);
|
|
121
|
+
// Create event handlers using the factory
|
|
122
|
+
const handlers = useCallback(() => ({
|
|
123
|
+
tags: {
|
|
124
|
+
INSERT: createEventHandler(dispatch.bind(null, addTag), "new", "Tag"),
|
|
125
|
+
UPDATE: createEventHandler(dispatch.bind(null, updateTag), "new", "Tag"),
|
|
126
|
+
DELETE: createEventHandler(dispatch.bind(null, deleteTag), "old", "Tag"),
|
|
127
|
+
},
|
|
128
|
+
devices: {
|
|
129
|
+
INSERT: createEventHandler(dispatch.bind(null, addDevice), "new", "Device"),
|
|
130
|
+
UPDATE: createEventHandler(dispatch.bind(null, updateDevice), "new", "Device"),
|
|
131
|
+
DELETE: createEventHandler(dispatch.bind(null, deleteDevice), "old", "Device"),
|
|
132
|
+
},
|
|
133
|
+
plans: {
|
|
134
|
+
INSERT: createEventHandler(dispatch.bind(null, addPlan), "new", "Plan"),
|
|
135
|
+
UPDATE: createEventHandler(dispatch.bind(null, updatePlan), "new", "Plan"),
|
|
136
|
+
DELETE: createEventHandler(dispatch.bind(null, deletePlan), "old", "Plan"),
|
|
137
|
+
},
|
|
138
|
+
sessions: {
|
|
139
|
+
INSERT: createEventHandler(dispatch.bind(null, addSessionToStore), "new", "Session"),
|
|
140
|
+
UPDATE: createEventHandler(dispatch.bind(null, updateSessionInStore), "new", "Session"),
|
|
141
|
+
DELETE: createEventHandler(dispatch.bind(null, deleteSessionFromStore), "old", "Session"),
|
|
142
|
+
},
|
|
143
|
+
connectivity: {
|
|
144
|
+
INSERT: (payload) => console.log("[DB Listener] Connectivity INSERT received:", payload),
|
|
145
|
+
UPDATE: (payload) => console.log("[DB Listener] Connectivity UPDATE received:", payload),
|
|
146
|
+
DELETE: (payload) => console.log("[DB Listener] Connectivity DELETE received:", payload),
|
|
147
|
+
},
|
|
148
|
+
}), [createEventHandler, dispatch]);
|
|
149
|
+
// Create a channel with proper error handling
|
|
150
|
+
const createChannel = useCallback((tableName) => {
|
|
151
|
+
if (!supabase.current)
|
|
152
|
+
return null;
|
|
153
|
+
const channelName = `scout_broadcast_${tableName}_${Date.now()}`;
|
|
154
|
+
console.log(`[DB Listener] Creating broadcast channel for ${tableName}:`, channelName);
|
|
155
|
+
try {
|
|
156
|
+
const channel = supabase.current.channel(channelName, {
|
|
157
|
+
config: { private: true },
|
|
158
|
+
});
|
|
159
|
+
// Add system event handlers for connection monitoring
|
|
160
|
+
channel
|
|
161
|
+
.on("system", { event: "disconnect" }, () => {
|
|
162
|
+
console.log(`[DB Listener] ๐ ${tableName} channel disconnected`);
|
|
163
|
+
setConnectionState(ConnectionState.DISCONNECTED);
|
|
164
|
+
setLastError("Channel disconnected");
|
|
165
|
+
})
|
|
166
|
+
.on("system", { event: "reconnect" }, () => {
|
|
167
|
+
console.log(`[DB Listener] ๐ ${tableName} channel reconnected`);
|
|
168
|
+
})
|
|
169
|
+
.on("system", { event: "error" }, (error) => {
|
|
170
|
+
console.warn(`[DB Listener] โ ${tableName} channel error:`, error);
|
|
171
|
+
setLastError(`Channel error: ${error}`);
|
|
172
|
+
});
|
|
173
|
+
return channel;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error(`[DB Listener] Failed to create ${tableName} channel:`, error);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}, []);
|
|
180
|
+
// Set up all channels
|
|
181
|
+
const setupChannels = useCallback(async () => {
|
|
182
|
+
if (!supabase.current)
|
|
183
|
+
return false;
|
|
184
|
+
cleanupChannels();
|
|
185
|
+
const tableHandlers = handlers();
|
|
186
|
+
const tables = Object.keys(tableHandlers);
|
|
187
|
+
let successCount = 0;
|
|
188
|
+
const totalChannels = tables.length;
|
|
189
|
+
for (const tableName of tables) {
|
|
190
|
+
const channel = createChannel(tableName);
|
|
191
|
+
if (!channel)
|
|
192
|
+
continue;
|
|
130
193
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
// Set up event handlers
|
|
195
|
+
const tableHandler = tableHandlers[tableName];
|
|
196
|
+
Object.entries(tableHandler).forEach(([event, handler]) => {
|
|
197
|
+
channel.on("broadcast", { event }, handler);
|
|
198
|
+
});
|
|
199
|
+
// Subscribe to the channel
|
|
200
|
+
channel.subscribe((status) => {
|
|
201
|
+
console.log(`[DB Listener] ${tableName} channel status:`, status);
|
|
202
|
+
if (status === "SUBSCRIBED") {
|
|
203
|
+
successCount++;
|
|
204
|
+
if (successCount === totalChannels) {
|
|
205
|
+
setConnectionState(ConnectionState.CONNECTED);
|
|
206
|
+
setRetryCount(0);
|
|
207
|
+
setLastError(null);
|
|
208
|
+
console.log("[DB Listener] โ
All channels successfully subscribed");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
212
|
+
console.error(`[DB Listener] ${tableName} channel failed to subscribe:`, status);
|
|
213
|
+
setLastError(`Channel subscription failed: ${status}`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
channels.current.push(channel);
|
|
133
217
|
}
|
|
134
|
-
catch (
|
|
135
|
-
console.
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error(`[DB Listener] Failed to set up ${tableName} channel:`, error);
|
|
136
220
|
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
console.log(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.on("broadcast", { event: "INSERT" }, (payload) => {
|
|
169
|
-
console.log("[DB Listener] Devices INSERT received:", payload);
|
|
170
|
-
handleDeviceInserts(payload);
|
|
171
|
-
})
|
|
172
|
-
.on("broadcast", { event: "UPDATE" }, (payload) => {
|
|
173
|
-
console.log("[DB Listener] Devices UPDATE received:", payload);
|
|
174
|
-
handleDeviceUpdates(payload);
|
|
175
|
-
})
|
|
176
|
-
.on("broadcast", { event: "DELETE" }, (payload) => {
|
|
177
|
-
console.log("[DB Listener] Devices DELETE received:", payload);
|
|
178
|
-
handleDeviceDeletes(payload);
|
|
179
|
-
})
|
|
180
|
-
.subscribe((status) => {
|
|
181
|
-
console.log(`[DB Listener] Devices channel status:`, status);
|
|
182
|
-
});
|
|
183
|
-
// Tags channel
|
|
184
|
-
const tagsChannel = createBroadcastChannel("tags");
|
|
185
|
-
tagsChannel
|
|
186
|
-
.on("broadcast", { event: "INSERT" }, (payload) => {
|
|
187
|
-
console.log("[DB Listener] Tags INSERT received:", payload);
|
|
188
|
-
handleTagInserts(payload);
|
|
189
|
-
})
|
|
190
|
-
.on("broadcast", { event: "UPDATE" }, (payload) => {
|
|
191
|
-
console.log("[DB Listener] Tags UPDATE received:", payload);
|
|
192
|
-
handleTagUpdates(payload);
|
|
193
|
-
})
|
|
194
|
-
.on("broadcast", { event: "DELETE" }, (payload) => {
|
|
195
|
-
console.log("[DB Listener] Tags DELETE received:", payload);
|
|
196
|
-
handleTagDeletes(payload);
|
|
197
|
-
})
|
|
198
|
-
.subscribe((status) => {
|
|
199
|
-
console.log(`[DB Listener] Tags channel status:`, status);
|
|
200
|
-
});
|
|
201
|
-
// Sessions channel
|
|
202
|
-
const sessionsChannel = createBroadcastChannel("sessions");
|
|
203
|
-
sessionsChannel
|
|
204
|
-
.on("broadcast", { event: "INSERT" }, (payload) => {
|
|
205
|
-
console.log("[DB Listener] Sessions INSERT received:", payload);
|
|
206
|
-
handleSessionInserts(payload);
|
|
207
|
-
})
|
|
208
|
-
.on("broadcast", { event: "UPDATE" }, (payload) => {
|
|
209
|
-
console.log("[DB Listener] Sessions UPDATE received:", payload);
|
|
210
|
-
handleSessionUpdates(payload);
|
|
211
|
-
})
|
|
212
|
-
.on("broadcast", { event: "DELETE" }, (payload) => {
|
|
213
|
-
console.log("[DB Listener] Sessions DELETE received:", payload);
|
|
214
|
-
handleSessionDeletes(payload);
|
|
215
|
-
})
|
|
216
|
-
.subscribe((status) => {
|
|
217
|
-
console.log(`[DB Listener] Sessions channel status:`, status);
|
|
218
|
-
});
|
|
219
|
-
// Connectivity channel
|
|
220
|
-
const connectivityChannel = createBroadcastChannel("connectivity");
|
|
221
|
-
connectivityChannel
|
|
222
|
-
.on("broadcast", { event: "INSERT" }, (payload) => {
|
|
223
|
-
console.log("[DB Listener] Connectivity INSERT received:", payload);
|
|
224
|
-
handleConnectivityInserts(payload);
|
|
225
|
-
})
|
|
226
|
-
.on("broadcast", { event: "UPDATE" }, (payload) => {
|
|
227
|
-
console.log("[DB Listener] Connectivity UPDATE received:", payload);
|
|
228
|
-
handleConnectivityUpdates(payload);
|
|
229
|
-
})
|
|
230
|
-
.on("broadcast", { event: "DELETE" }, (payload) => {
|
|
231
|
-
console.log("[DB Listener] Connectivity DELETE received:", payload);
|
|
232
|
-
handleConnectivityDeletes(payload);
|
|
233
|
-
})
|
|
234
|
-
.subscribe((status) => {
|
|
235
|
-
console.log(`[DB Listener] Connectivity channel status:`, status);
|
|
236
|
-
});
|
|
237
|
-
// Add all channels to the channels array
|
|
238
|
-
channels.current.push(plansChannel, devicesChannel, tagsChannel, sessionsChannel, connectivityChannel);
|
|
239
|
-
// Test the connection with system events
|
|
240
|
-
const testChannelName = `test_connection_${Date.now()}`;
|
|
241
|
-
console.log("[DB Listener] Creating test channel:", testChannelName);
|
|
242
|
-
const testChannel = scoutSupabase.channel(testChannelName);
|
|
243
|
-
testChannel
|
|
244
|
-
.on("system", { event: "disconnect" }, () => {
|
|
245
|
-
console.log("[DB Listener] ๐ Disconnected from Supabase");
|
|
246
|
-
})
|
|
247
|
-
.on("system", { event: "reconnect" }, () => {
|
|
248
|
-
console.log("[DB Listener] ๐ Reconnected to Supabase");
|
|
249
|
-
})
|
|
250
|
-
.on("system", { event: "error" }, (error) => {
|
|
251
|
-
console.warn("[DB Listener] โ System error:", error);
|
|
252
|
-
})
|
|
253
|
-
.subscribe((status) => {
|
|
254
|
-
console.log("[DB Listener] Test channel status:", status);
|
|
255
|
-
});
|
|
256
|
-
channels.current.push(testChannel);
|
|
257
|
-
// Test a simple database query to verify connection
|
|
258
|
-
const testDbConnection = async () => {
|
|
259
|
-
try {
|
|
260
|
-
const { data, error } = await scoutSupabase
|
|
261
|
-
.from("tags")
|
|
262
|
-
.select("count")
|
|
263
|
-
.limit(1);
|
|
264
|
-
console.log("[DB Listener] DB connection test - Success:", !!data);
|
|
265
|
-
console.log("[DB Listener] DB connection test - Error:", error);
|
|
221
|
+
}
|
|
222
|
+
return successCount > 0;
|
|
223
|
+
}, [cleanupChannels, createChannel, handlers]);
|
|
224
|
+
// Schedule reconnection with exponential backoff
|
|
225
|
+
const scheduleReconnection = useCallback(() => {
|
|
226
|
+
if (isDestroyedRef.current ||
|
|
227
|
+
retryCount >= RECONNECTION_CONFIG.MAX_RETRIES) {
|
|
228
|
+
console.log("[DB Listener] Max reconnection attempts reached or hook destroyed");
|
|
229
|
+
setConnectionState(ConnectionState.ERROR);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const delay = calculateBackoffDelay(retryCount);
|
|
233
|
+
console.log(`[DB Listener] Scheduling reconnection attempt ${retryCount + 1} in ${delay}ms`);
|
|
234
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
235
|
+
if (!isDestroyedRef.current) {
|
|
236
|
+
initializeConnection();
|
|
237
|
+
}
|
|
238
|
+
}, delay);
|
|
239
|
+
}, [retryCount, calculateBackoffDelay]);
|
|
240
|
+
// Initialize connection
|
|
241
|
+
const initializeConnection = useCallback(async () => {
|
|
242
|
+
if (isDestroyedRef.current || isInitializingRef.current)
|
|
243
|
+
return;
|
|
244
|
+
isInitializingRef.current = true;
|
|
245
|
+
setConnectionState(ConnectionState.CONNECTING);
|
|
246
|
+
try {
|
|
247
|
+
console.log("[DB Listener] ๐ Initializing connection...");
|
|
248
|
+
// Test database connection
|
|
249
|
+
const dbConnected = await testDbConnection();
|
|
250
|
+
if (!dbConnected) {
|
|
251
|
+
throw new Error("Database connection test failed");
|
|
266
252
|
}
|
|
267
|
-
|
|
268
|
-
|
|
253
|
+
// Set up realtime authentication
|
|
254
|
+
const authSuccess = await setupRealtimeAuth();
|
|
255
|
+
if (!authSuccess) {
|
|
256
|
+
throw new Error("Realtime authentication failed");
|
|
269
257
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
258
|
+
// Set up channels
|
|
259
|
+
const channelsSuccess = await setupChannels();
|
|
260
|
+
if (!channelsSuccess) {
|
|
261
|
+
throw new Error("Channel setup failed");
|
|
262
|
+
}
|
|
263
|
+
console.log("[DB Listener] โ
Connection initialized successfully");
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.error("[DB Listener] โ Connection initialization failed:", error);
|
|
267
|
+
setLastError(error instanceof Error ? error.message : "Unknown error");
|
|
268
|
+
setConnectionState(ConnectionState.ERROR);
|
|
269
|
+
setRetryCount((prev) => prev + 1);
|
|
270
|
+
// Schedule reconnection
|
|
271
|
+
scheduleReconnection();
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
isInitializingRef.current = false;
|
|
275
|
+
}
|
|
276
|
+
}, [
|
|
277
|
+
testDbConnection,
|
|
278
|
+
setupRealtimeAuth,
|
|
279
|
+
setupChannels,
|
|
280
|
+
scheduleReconnection,
|
|
281
|
+
]);
|
|
282
|
+
// Manual reconnection function
|
|
283
|
+
const reconnect = useCallback(() => {
|
|
284
|
+
if (isDestroyedRef.current)
|
|
285
|
+
return;
|
|
286
|
+
console.log("[DB Listener] ๐ Manual reconnection requested");
|
|
287
|
+
cancelReconnection();
|
|
288
|
+
setRetryCount(0);
|
|
289
|
+
setLastError(null);
|
|
290
|
+
initializeConnection();
|
|
291
|
+
}, [cancelReconnection, initializeConnection]);
|
|
292
|
+
// Main effect
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
console.log("=== SCOUT DB LISTENER INITIALIZATION ===");
|
|
295
|
+
if (!scoutSupabase) {
|
|
296
|
+
console.error("[DB Listener] No Supabase client available");
|
|
297
|
+
setConnectionState(ConnectionState.ERROR);
|
|
298
|
+
setLastError("No Supabase client available");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
supabase.current = scoutSupabase;
|
|
302
|
+
isDestroyedRef.current = false;
|
|
303
|
+
// Initialize connection
|
|
304
|
+
initializeConnection();
|
|
273
305
|
// Cleanup function
|
|
274
306
|
return () => {
|
|
275
|
-
console.log("[DB Listener] ๐งน Cleaning up
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
channels.current = [];
|
|
307
|
+
console.log("[DB Listener] ๐งน Cleaning up hook");
|
|
308
|
+
isDestroyedRef.current = true;
|
|
309
|
+
cancelReconnection();
|
|
310
|
+
cleanupChannels();
|
|
282
311
|
};
|
|
283
|
-
}, [
|
|
312
|
+
}, [
|
|
313
|
+
scoutSupabase,
|
|
314
|
+
initializeConnection,
|
|
315
|
+
cancelReconnection,
|
|
316
|
+
cleanupChannels,
|
|
317
|
+
]);
|
|
318
|
+
// Return connection state and manual reconnect function
|
|
319
|
+
return {
|
|
320
|
+
connectionState,
|
|
321
|
+
lastError,
|
|
322
|
+
retryCount,
|
|
323
|
+
reconnect,
|
|
324
|
+
isConnected: connectionState === ConnectionState.CONNECTED,
|
|
325
|
+
isConnecting: connectionState === ConnectionState.CONNECTING,
|
|
326
|
+
};
|
|
284
327
|
}
|
|
@@ -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,18 @@ export function ScoutRefreshProvider({ children }) {
|
|
|
23
32
|
supabaseRef.current = createBrowserClient(url, anon_key);
|
|
24
33
|
console.log("[ScoutRefreshProvider] Created Supabase client");
|
|
25
34
|
}
|
|
35
|
+
// Use the enhanced DB listener with connection status
|
|
26
36
|
useScoutDbListener(supabaseRef.current);
|
|
27
37
|
useScoutRefresh();
|
|
38
|
+
// // Log connection status changes for debugging
|
|
39
|
+
// if (connectionStatus.lastError) {
|
|
40
|
+
// console.warn(
|
|
41
|
+
// "[ScoutRefreshProvider] DB Listener error:",
|
|
42
|
+
// connectionStatus.lastError
|
|
43
|
+
// );
|
|
44
|
+
// }
|
|
45
|
+
// if (connectionStatus.isConnected) {
|
|
46
|
+
// console.log("[ScoutRefreshProvider] โ
DB Listener connected");
|
|
47
|
+
// }
|
|
28
48
|
return (_jsx(SupabaseContext.Provider, { value: supabaseRef.current, children: children }));
|
|
29
49
|
}
|
package/dist/store/scout.js
CHANGED
|
@@ -68,7 +68,10 @@ export const scoutSlice = createSlice({
|
|
|
68
68
|
addTag(state, action) {
|
|
69
69
|
for (const herd_module of state.herd_modules) {
|
|
70
70
|
for (const event of herd_module.events) {
|
|
71
|
-
if (event.id === action.payload.event_id
|
|
71
|
+
if (event.id === action.payload.event_id) {
|
|
72
|
+
if (event.tags == undefined || event.tags == null) {
|
|
73
|
+
event.tags = [];
|
|
74
|
+
}
|
|
72
75
|
event.tags.push(action.payload);
|
|
73
76
|
return;
|
|
74
77
|
}
|