@oxyhq/auth 1.1.4 → 2.0.0
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.
|
@@ -9,63 +9,76 @@ const socket_io_client_1 = __importDefault(require("socket.io-client"));
|
|
|
9
9
|
const sonner_1 = require("sonner");
|
|
10
10
|
const core_1 = require("@oxyhq/core");
|
|
11
11
|
const core_2 = require("@oxyhq/core");
|
|
12
|
+
const WebOxyProvider_1 = require("../WebOxyProvider");
|
|
13
|
+
const react_query_1 = require("@tanstack/react-query");
|
|
14
|
+
const useServicesQueries_1 = require("./queries/useServicesQueries");
|
|
15
|
+
const queryKeys_1 = require("./queries/queryKeys");
|
|
12
16
|
const debug = (0, core_2.createDebugLogger)('SessionSocket');
|
|
13
|
-
function useSessionSocket(
|
|
17
|
+
function useSessionSocket(options) {
|
|
18
|
+
const { user, activeSessionId, oxyServices, signOut, clearSessionState } = (0, WebOxyProvider_1.useWebOxy)();
|
|
19
|
+
const queryClient = (0, react_query_1.useQueryClient)();
|
|
20
|
+
const userId = user?.id ?? null;
|
|
21
|
+
const baseURL = oxyServices.getBaseURL();
|
|
22
|
+
// Derive currentDeviceId from sessions query
|
|
23
|
+
const { data: sessions } = (0, useServicesQueries_1.useSessions)(userId ?? undefined);
|
|
24
|
+
const currentDeviceId = (0, react_1.useMemo)(() => {
|
|
25
|
+
if (!sessions || !activeSessionId)
|
|
26
|
+
return null;
|
|
27
|
+
const active = sessions.find((s) => s.sessionId === activeSessionId);
|
|
28
|
+
return active?.deviceId ?? null;
|
|
29
|
+
}, [sessions, activeSessionId]);
|
|
14
30
|
const socketRef = (0, react_1.useRef)(null);
|
|
15
|
-
|
|
16
|
-
// Store callbacks in refs to avoid re-joining when they change
|
|
17
|
-
const refreshSessionsRef = (0, react_1.useRef)(refreshSessions);
|
|
18
|
-
const logoutRef = (0, react_1.useRef)(logout);
|
|
31
|
+
// Store callbacks and values in refs to avoid reconnecting when they change
|
|
19
32
|
const clearSessionStateRef = (0, react_1.useRef)(clearSessionState);
|
|
20
|
-
const onRemoteSignOutRef = (0, react_1.useRef)(onRemoteSignOut);
|
|
21
|
-
const onSessionRemovedRef = (0, react_1.useRef)(onSessionRemoved);
|
|
33
|
+
const onRemoteSignOutRef = (0, react_1.useRef)(options?.onRemoteSignOut);
|
|
34
|
+
const onSessionRemovedRef = (0, react_1.useRef)(options?.onSessionRemoved);
|
|
22
35
|
const activeSessionIdRef = (0, react_1.useRef)(activeSessionId);
|
|
23
36
|
const currentDeviceIdRef = (0, react_1.useRef)(currentDeviceId);
|
|
24
|
-
|
|
37
|
+
const queryClientRef = (0, react_1.useRef)(queryClient);
|
|
38
|
+
// Update refs when values change
|
|
25
39
|
(0, react_1.useEffect)(() => {
|
|
26
|
-
refreshSessionsRef.current = refreshSessions;
|
|
27
|
-
logoutRef.current = logout;
|
|
28
40
|
clearSessionStateRef.current = clearSessionState;
|
|
29
|
-
onRemoteSignOutRef.current = onRemoteSignOut;
|
|
30
|
-
onSessionRemovedRef.current = onSessionRemoved;
|
|
41
|
+
onRemoteSignOutRef.current = options?.onRemoteSignOut;
|
|
42
|
+
onSessionRemovedRef.current = options?.onSessionRemoved;
|
|
31
43
|
activeSessionIdRef.current = activeSessionId;
|
|
32
44
|
currentDeviceIdRef.current = currentDeviceId;
|
|
33
|
-
|
|
45
|
+
queryClientRef.current = queryClient;
|
|
46
|
+
}, [clearSessionState, options?.onRemoteSignOut, options?.onSessionRemoved, activeSessionId, currentDeviceId, queryClient]);
|
|
34
47
|
(0, react_1.useEffect)(() => {
|
|
35
48
|
if (!userId || !baseURL) {
|
|
36
49
|
// Clean up if userId or baseURL becomes invalid
|
|
37
|
-
if (socketRef.current
|
|
38
|
-
socketRef.current.
|
|
39
|
-
|
|
50
|
+
if (socketRef.current) {
|
|
51
|
+
socketRef.current.disconnect();
|
|
52
|
+
socketRef.current = null;
|
|
40
53
|
}
|
|
41
54
|
return;
|
|
42
55
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
socketRef.current =
|
|
47
|
-
transports: ['websocket'],
|
|
48
|
-
});
|
|
56
|
+
// Disconnect previous socket if switching users
|
|
57
|
+
if (socketRef.current) {
|
|
58
|
+
socketRef.current.disconnect();
|
|
59
|
+
socketRef.current = null;
|
|
49
60
|
}
|
|
61
|
+
// Connect with auth token; use callback so reconnections get a fresh token
|
|
62
|
+
socketRef.current = (0, socket_io_client_1.default)(baseURL, {
|
|
63
|
+
transports: ['websocket'],
|
|
64
|
+
auth: (cb) => {
|
|
65
|
+
const token = oxyServices.getAccessToken();
|
|
66
|
+
cb({ token: token ?? '' });
|
|
67
|
+
},
|
|
68
|
+
});
|
|
50
69
|
const socket = socketRef.current;
|
|
51
|
-
//
|
|
52
|
-
if (joinedRoomRef.current !== roomId) {
|
|
53
|
-
// Leave previous room if switching users
|
|
54
|
-
if (joinedRoomRef.current) {
|
|
55
|
-
socket.emit('leave', { userId: joinedRoomRef.current });
|
|
56
|
-
}
|
|
57
|
-
socket.emit('join', { userId: roomId });
|
|
58
|
-
joinedRoomRef.current = roomId;
|
|
59
|
-
debug.log('Emitting join for room:', roomId);
|
|
60
|
-
}
|
|
61
|
-
// Set up event handlers (only once per socket instance)
|
|
70
|
+
// Server auto-joins the user to `user:<userId>` room on connection
|
|
62
71
|
const handleConnect = () => {
|
|
63
72
|
debug.log('Socket connected:', socket.id);
|
|
64
73
|
};
|
|
74
|
+
const refreshSessions = () => {
|
|
75
|
+
(0, queryKeys_1.invalidateSessionQueries)(queryClientRef.current);
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
};
|
|
65
78
|
const handleSessionUpdate = async (data) => {
|
|
66
79
|
debug.log('Received session_update:', data);
|
|
67
80
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
68
|
-
const
|
|
81
|
+
const deviceId = currentDeviceIdRef.current;
|
|
69
82
|
// Handle different event types
|
|
70
83
|
if (data.type === 'session_removed') {
|
|
71
84
|
// Track removed session
|
|
@@ -80,8 +93,6 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
80
93
|
else {
|
|
81
94
|
sonner_1.toast.info('You have been signed out remotely.');
|
|
82
95
|
}
|
|
83
|
-
// Use clearSessionState since session was already removed server-side
|
|
84
|
-
// Await to ensure storage cleanup completes before continuing
|
|
85
96
|
try {
|
|
86
97
|
await clearSessionStateRef.current();
|
|
87
98
|
}
|
|
@@ -92,13 +103,7 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
else {
|
|
95
|
-
|
|
96
|
-
refreshSessionsRef.current().catch((error) => {
|
|
97
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
98
|
-
if (__DEV__) {
|
|
99
|
-
core_1.logger.debug('Failed to refresh sessions after session_removed', { component: 'useSessionSocket' }, error);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
106
|
+
refreshSessions();
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
else if (data.type === 'device_removed') {
|
|
@@ -109,15 +114,13 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
// If the removed deviceId matches the current device, immediately clear state
|
|
112
|
-
if (data.deviceId && data.deviceId ===
|
|
117
|
+
if (data.deviceId && data.deviceId === deviceId) {
|
|
113
118
|
if (onRemoteSignOutRef.current) {
|
|
114
119
|
onRemoteSignOutRef.current();
|
|
115
120
|
}
|
|
116
121
|
else {
|
|
117
122
|
sonner_1.toast.info('This device has been removed. You have been signed out.');
|
|
118
123
|
}
|
|
119
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
120
|
-
// Await to ensure storage cleanup completes before continuing
|
|
121
124
|
try {
|
|
122
125
|
await clearSessionStateRef.current();
|
|
123
126
|
}
|
|
@@ -128,13 +131,7 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
else {
|
|
131
|
-
|
|
132
|
-
refreshSessionsRef.current().catch((error) => {
|
|
133
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
134
|
-
if (__DEV__) {
|
|
135
|
-
core_1.logger.debug('Failed to refresh sessions after device_removed', { component: 'useSessionSocket' }, error);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
134
|
+
refreshSessions();
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
else if (data.type === 'sessions_removed') {
|
|
@@ -152,8 +149,6 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
152
149
|
else {
|
|
153
150
|
sonner_1.toast.info('You have been signed out remotely.');
|
|
154
151
|
}
|
|
155
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
156
|
-
// Await to ensure storage cleanup completes before continuing
|
|
157
152
|
try {
|
|
158
153
|
await clearSessionStateRef.current();
|
|
159
154
|
}
|
|
@@ -164,23 +159,12 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
164
159
|
}
|
|
165
160
|
}
|
|
166
161
|
else {
|
|
167
|
-
|
|
168
|
-
refreshSessionsRef.current().catch((error) => {
|
|
169
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
170
|
-
if (__DEV__) {
|
|
171
|
-
core_1.logger.debug('Failed to refresh sessions after sessions_removed', { component: 'useSessionSocket' }, error);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
162
|
+
refreshSessions();
|
|
174
163
|
}
|
|
175
164
|
}
|
|
176
165
|
else {
|
|
177
|
-
// For other event types (e.g., session_created), refresh sessions
|
|
178
|
-
|
|
179
|
-
// Log but don't throw - refresh errors shouldn't break the socket handler
|
|
180
|
-
if (__DEV__) {
|
|
181
|
-
core_1.logger.debug('Failed to refresh sessions after session_update', { component: 'useSessionSocket' }, error);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
166
|
+
// For other event types (e.g., session_created), refresh sessions
|
|
167
|
+
refreshSessions();
|
|
184
168
|
// If the current session was logged out (legacy behavior), handle it specially
|
|
185
169
|
if (data.sessionId === currentActiveSessionId) {
|
|
186
170
|
if (onRemoteSignOutRef.current) {
|
|
@@ -189,8 +173,6 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
189
173
|
else {
|
|
190
174
|
sonner_1.toast.info('You have been signed out remotely.');
|
|
191
175
|
}
|
|
192
|
-
// Use clearSessionState since session was already removed server-side
|
|
193
|
-
// Await to ensure storage cleanup completes before continuing
|
|
194
176
|
try {
|
|
195
177
|
await clearSessionStateRef.current();
|
|
196
178
|
}
|
|
@@ -205,11 +187,8 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
205
187
|
return () => {
|
|
206
188
|
socket.off('connect', handleConnect);
|
|
207
189
|
socket.off('session_update', handleSessionUpdate);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
socket.emit('leave', { userId: roomId });
|
|
211
|
-
joinedRoomRef.current = null;
|
|
212
|
-
}
|
|
190
|
+
socket.disconnect();
|
|
191
|
+
socketRef.current = null;
|
|
213
192
|
};
|
|
214
193
|
}, [userId, baseURL]); // Only depend on userId and baseURL - callbacks are in refs
|
|
215
194
|
}
|
|
@@ -1,65 +1,78 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import io from 'socket.io-client';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
import { logger } from '@oxyhq/core';
|
|
5
5
|
import { createDebugLogger } from '@oxyhq/core';
|
|
6
|
+
import { useWebOxy } from '../WebOxyProvider';
|
|
7
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import { useSessions } from './queries/useServicesQueries';
|
|
9
|
+
import { invalidateSessionQueries } from './queries/queryKeys';
|
|
6
10
|
const debug = createDebugLogger('SessionSocket');
|
|
7
|
-
export function useSessionSocket(
|
|
11
|
+
export function useSessionSocket(options) {
|
|
12
|
+
const { user, activeSessionId, oxyServices, signOut, clearSessionState } = useWebOxy();
|
|
13
|
+
const queryClient = useQueryClient();
|
|
14
|
+
const userId = user?.id ?? null;
|
|
15
|
+
const baseURL = oxyServices.getBaseURL();
|
|
16
|
+
// Derive currentDeviceId from sessions query
|
|
17
|
+
const { data: sessions } = useSessions(userId ?? undefined);
|
|
18
|
+
const currentDeviceId = useMemo(() => {
|
|
19
|
+
if (!sessions || !activeSessionId)
|
|
20
|
+
return null;
|
|
21
|
+
const active = sessions.find((s) => s.sessionId === activeSessionId);
|
|
22
|
+
return active?.deviceId ?? null;
|
|
23
|
+
}, [sessions, activeSessionId]);
|
|
8
24
|
const socketRef = useRef(null);
|
|
9
|
-
|
|
10
|
-
// Store callbacks in refs to avoid re-joining when they change
|
|
11
|
-
const refreshSessionsRef = useRef(refreshSessions);
|
|
12
|
-
const logoutRef = useRef(logout);
|
|
25
|
+
// Store callbacks and values in refs to avoid reconnecting when they change
|
|
13
26
|
const clearSessionStateRef = useRef(clearSessionState);
|
|
14
|
-
const onRemoteSignOutRef = useRef(onRemoteSignOut);
|
|
15
|
-
const onSessionRemovedRef = useRef(onSessionRemoved);
|
|
27
|
+
const onRemoteSignOutRef = useRef(options?.onRemoteSignOut);
|
|
28
|
+
const onSessionRemovedRef = useRef(options?.onSessionRemoved);
|
|
16
29
|
const activeSessionIdRef = useRef(activeSessionId);
|
|
17
30
|
const currentDeviceIdRef = useRef(currentDeviceId);
|
|
18
|
-
|
|
31
|
+
const queryClientRef = useRef(queryClient);
|
|
32
|
+
// Update refs when values change
|
|
19
33
|
useEffect(() => {
|
|
20
|
-
refreshSessionsRef.current = refreshSessions;
|
|
21
|
-
logoutRef.current = logout;
|
|
22
34
|
clearSessionStateRef.current = clearSessionState;
|
|
23
|
-
onRemoteSignOutRef.current = onRemoteSignOut;
|
|
24
|
-
onSessionRemovedRef.current = onSessionRemoved;
|
|
35
|
+
onRemoteSignOutRef.current = options?.onRemoteSignOut;
|
|
36
|
+
onSessionRemovedRef.current = options?.onSessionRemoved;
|
|
25
37
|
activeSessionIdRef.current = activeSessionId;
|
|
26
38
|
currentDeviceIdRef.current = currentDeviceId;
|
|
27
|
-
|
|
39
|
+
queryClientRef.current = queryClient;
|
|
40
|
+
}, [clearSessionState, options?.onRemoteSignOut, options?.onSessionRemoved, activeSessionId, currentDeviceId, queryClient]);
|
|
28
41
|
useEffect(() => {
|
|
29
42
|
if (!userId || !baseURL) {
|
|
30
43
|
// Clean up if userId or baseURL becomes invalid
|
|
31
|
-
if (socketRef.current
|
|
32
|
-
socketRef.current.
|
|
33
|
-
|
|
44
|
+
if (socketRef.current) {
|
|
45
|
+
socketRef.current.disconnect();
|
|
46
|
+
socketRef.current = null;
|
|
34
47
|
}
|
|
35
48
|
return;
|
|
36
49
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
socketRef.current =
|
|
41
|
-
transports: ['websocket'],
|
|
42
|
-
});
|
|
50
|
+
// Disconnect previous socket if switching users
|
|
51
|
+
if (socketRef.current) {
|
|
52
|
+
socketRef.current.disconnect();
|
|
53
|
+
socketRef.current = null;
|
|
43
54
|
}
|
|
55
|
+
// Connect with auth token; use callback so reconnections get a fresh token
|
|
56
|
+
socketRef.current = io(baseURL, {
|
|
57
|
+
transports: ['websocket'],
|
|
58
|
+
auth: (cb) => {
|
|
59
|
+
const token = oxyServices.getAccessToken();
|
|
60
|
+
cb({ token: token ?? '' });
|
|
61
|
+
},
|
|
62
|
+
});
|
|
44
63
|
const socket = socketRef.current;
|
|
45
|
-
//
|
|
46
|
-
if (joinedRoomRef.current !== roomId) {
|
|
47
|
-
// Leave previous room if switching users
|
|
48
|
-
if (joinedRoomRef.current) {
|
|
49
|
-
socket.emit('leave', { userId: joinedRoomRef.current });
|
|
50
|
-
}
|
|
51
|
-
socket.emit('join', { userId: roomId });
|
|
52
|
-
joinedRoomRef.current = roomId;
|
|
53
|
-
debug.log('Emitting join for room:', roomId);
|
|
54
|
-
}
|
|
55
|
-
// Set up event handlers (only once per socket instance)
|
|
64
|
+
// Server auto-joins the user to `user:<userId>` room on connection
|
|
56
65
|
const handleConnect = () => {
|
|
57
66
|
debug.log('Socket connected:', socket.id);
|
|
58
67
|
};
|
|
68
|
+
const refreshSessions = () => {
|
|
69
|
+
invalidateSessionQueries(queryClientRef.current);
|
|
70
|
+
return Promise.resolve();
|
|
71
|
+
};
|
|
59
72
|
const handleSessionUpdate = async (data) => {
|
|
60
73
|
debug.log('Received session_update:', data);
|
|
61
74
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
62
|
-
const
|
|
75
|
+
const deviceId = currentDeviceIdRef.current;
|
|
63
76
|
// Handle different event types
|
|
64
77
|
if (data.type === 'session_removed') {
|
|
65
78
|
// Track removed session
|
|
@@ -74,8 +87,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
74
87
|
else {
|
|
75
88
|
toast.info('You have been signed out remotely.');
|
|
76
89
|
}
|
|
77
|
-
// Use clearSessionState since session was already removed server-side
|
|
78
|
-
// Await to ensure storage cleanup completes before continuing
|
|
79
90
|
try {
|
|
80
91
|
await clearSessionStateRef.current();
|
|
81
92
|
}
|
|
@@ -86,13 +97,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
else {
|
|
89
|
-
|
|
90
|
-
refreshSessionsRef.current().catch((error) => {
|
|
91
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
92
|
-
if (__DEV__) {
|
|
93
|
-
logger.debug('Failed to refresh sessions after session_removed', { component: 'useSessionSocket' }, error);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
100
|
+
refreshSessions();
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
else if (data.type === 'device_removed') {
|
|
@@ -103,15 +108,13 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
// If the removed deviceId matches the current device, immediately clear state
|
|
106
|
-
if (data.deviceId && data.deviceId ===
|
|
111
|
+
if (data.deviceId && data.deviceId === deviceId) {
|
|
107
112
|
if (onRemoteSignOutRef.current) {
|
|
108
113
|
onRemoteSignOutRef.current();
|
|
109
114
|
}
|
|
110
115
|
else {
|
|
111
116
|
toast.info('This device has been removed. You have been signed out.');
|
|
112
117
|
}
|
|
113
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
114
|
-
// Await to ensure storage cleanup completes before continuing
|
|
115
118
|
try {
|
|
116
119
|
await clearSessionStateRef.current();
|
|
117
120
|
}
|
|
@@ -122,13 +125,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
else {
|
|
125
|
-
|
|
126
|
-
refreshSessionsRef.current().catch((error) => {
|
|
127
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
128
|
-
if (__DEV__) {
|
|
129
|
-
logger.debug('Failed to refresh sessions after device_removed', { component: 'useSessionSocket' }, error);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
128
|
+
refreshSessions();
|
|
132
129
|
}
|
|
133
130
|
}
|
|
134
131
|
else if (data.type === 'sessions_removed') {
|
|
@@ -146,8 +143,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
146
143
|
else {
|
|
147
144
|
toast.info('You have been signed out remotely.');
|
|
148
145
|
}
|
|
149
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
150
|
-
// Await to ensure storage cleanup completes before continuing
|
|
151
146
|
try {
|
|
152
147
|
await clearSessionStateRef.current();
|
|
153
148
|
}
|
|
@@ -158,23 +153,12 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
158
153
|
}
|
|
159
154
|
}
|
|
160
155
|
else {
|
|
161
|
-
|
|
162
|
-
refreshSessionsRef.current().catch((error) => {
|
|
163
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
164
|
-
if (__DEV__) {
|
|
165
|
-
logger.debug('Failed to refresh sessions after sessions_removed', { component: 'useSessionSocket' }, error);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
156
|
+
refreshSessions();
|
|
168
157
|
}
|
|
169
158
|
}
|
|
170
159
|
else {
|
|
171
|
-
// For other event types (e.g., session_created), refresh sessions
|
|
172
|
-
|
|
173
|
-
// Log but don't throw - refresh errors shouldn't break the socket handler
|
|
174
|
-
if (__DEV__) {
|
|
175
|
-
logger.debug('Failed to refresh sessions after session_update', { component: 'useSessionSocket' }, error);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
160
|
+
// For other event types (e.g., session_created), refresh sessions
|
|
161
|
+
refreshSessions();
|
|
178
162
|
// If the current session was logged out (legacy behavior), handle it specially
|
|
179
163
|
if (data.sessionId === currentActiveSessionId) {
|
|
180
164
|
if (onRemoteSignOutRef.current) {
|
|
@@ -183,8 +167,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
183
167
|
else {
|
|
184
168
|
toast.info('You have been signed out remotely.');
|
|
185
169
|
}
|
|
186
|
-
// Use clearSessionState since session was already removed server-side
|
|
187
|
-
// Await to ensure storage cleanup completes before continuing
|
|
188
170
|
try {
|
|
189
171
|
await clearSessionStateRef.current();
|
|
190
172
|
}
|
|
@@ -199,11 +181,8 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
199
181
|
return () => {
|
|
200
182
|
socket.off('connect', handleConnect);
|
|
201
183
|
socket.off('session_update', handleSessionUpdate);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
socket.emit('leave', { userId: roomId });
|
|
205
|
-
joinedRoomRef.current = null;
|
|
206
|
-
}
|
|
184
|
+
socket.disconnect();
|
|
185
|
+
socketRef.current = null;
|
|
207
186
|
};
|
|
208
187
|
}, [userId, baseURL]); // Only depend on userId and baseURL - callbacks are in refs
|
|
209
188
|
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
interface
|
|
2
|
-
userId: string | null | undefined;
|
|
3
|
-
activeSessionId: string | null | undefined;
|
|
4
|
-
currentDeviceId: string | null | undefined;
|
|
5
|
-
refreshSessions: () => Promise<void>;
|
|
6
|
-
logout: () => Promise<void>;
|
|
7
|
-
clearSessionState: () => Promise<void>;
|
|
8
|
-
baseURL: string;
|
|
1
|
+
export interface UseSessionSocketOptions {
|
|
9
2
|
onRemoteSignOut?: () => void;
|
|
10
3
|
onSessionRemoved?: (sessionId: string) => void;
|
|
11
4
|
}
|
|
12
|
-
export declare function useSessionSocket(
|
|
13
|
-
export {};
|
|
5
|
+
export declare function useSessionSocket(options?: UseSessionSocketOptions): void;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { useUpdateProfile, useUploadAvatar, useUpdateAccountSettings, useUpdateP
|
|
|
31
31
|
export { createProfileMutation, createGenericMutation, } from './hooks/mutations/mutationFactory';
|
|
32
32
|
export type { ProfileMutationConfig, GenericMutationConfig, } from './hooks/mutations/mutationFactory';
|
|
33
33
|
export { useSessionSocket } from './hooks/useSessionSocket';
|
|
34
|
+
export type { UseSessionSocketOptions } from './hooks/useSessionSocket';
|
|
34
35
|
export { useAssets, setOxyAssetInstance } from './hooks/useAssets';
|
|
35
36
|
export { useFileDownloadUrl, setOxyFileUrlInstance } from './hooks/useFileDownloadUrl';
|
|
36
37
|
export { useFollow, useFollowerCounts } from './hooks/useFollow';
|
package/package.json
CHANGED
|
@@ -1,85 +1,91 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import io from 'socket.io-client';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
4
|
import { logger } from '@oxyhq/core';
|
|
5
5
|
import { createDebugLogger } from '@oxyhq/core';
|
|
6
|
+
import { useWebOxy } from '../WebOxyProvider';
|
|
7
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import { useSessions } from './queries/useServicesQueries';
|
|
9
|
+
import { invalidateSessionQueries } from './queries/queryKeys';
|
|
6
10
|
|
|
7
11
|
const debug = createDebugLogger('SessionSocket');
|
|
8
12
|
|
|
9
|
-
interface
|
|
10
|
-
userId: string | null | undefined;
|
|
11
|
-
activeSessionId: string | null | undefined;
|
|
12
|
-
currentDeviceId: string | null | undefined;
|
|
13
|
-
refreshSessions: () => Promise<void>;
|
|
14
|
-
logout: () => Promise<void>;
|
|
15
|
-
clearSessionState: () => Promise<void>;
|
|
16
|
-
baseURL: string;
|
|
13
|
+
export interface UseSessionSocketOptions {
|
|
17
14
|
onRemoteSignOut?: () => void;
|
|
18
15
|
onSessionRemoved?: (sessionId: string) => void;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
export function useSessionSocket(
|
|
18
|
+
export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
19
|
+
const { user, activeSessionId, oxyServices, signOut, clearSessionState } = useWebOxy();
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
|
|
22
|
+
const userId = user?.id ?? null;
|
|
23
|
+
const baseURL = oxyServices.getBaseURL();
|
|
24
|
+
|
|
25
|
+
// Derive currentDeviceId from sessions query
|
|
26
|
+
const { data: sessions } = useSessions(userId ?? undefined);
|
|
27
|
+
const currentDeviceId = useMemo(() => {
|
|
28
|
+
if (!sessions || !activeSessionId) return null;
|
|
29
|
+
const active = sessions.find((s) => s.sessionId === activeSessionId);
|
|
30
|
+
return active?.deviceId ?? null;
|
|
31
|
+
}, [sessions, activeSessionId]);
|
|
32
|
+
|
|
22
33
|
const socketRef = useRef<any>(null);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Store callbacks in refs to avoid re-joining when they change
|
|
26
|
-
const refreshSessionsRef = useRef(refreshSessions);
|
|
27
|
-
const logoutRef = useRef(logout);
|
|
34
|
+
|
|
35
|
+
// Store callbacks and values in refs to avoid reconnecting when they change
|
|
28
36
|
const clearSessionStateRef = useRef(clearSessionState);
|
|
29
|
-
const onRemoteSignOutRef = useRef(onRemoteSignOut);
|
|
30
|
-
const onSessionRemovedRef = useRef(onSessionRemoved);
|
|
37
|
+
const onRemoteSignOutRef = useRef(options?.onRemoteSignOut);
|
|
38
|
+
const onSessionRemovedRef = useRef(options?.onSessionRemoved);
|
|
31
39
|
const activeSessionIdRef = useRef(activeSessionId);
|
|
32
40
|
const currentDeviceIdRef = useRef(currentDeviceId);
|
|
41
|
+
const queryClientRef = useRef(queryClient);
|
|
33
42
|
|
|
34
|
-
// Update refs when
|
|
43
|
+
// Update refs when values change
|
|
35
44
|
useEffect(() => {
|
|
36
|
-
refreshSessionsRef.current = refreshSessions;
|
|
37
|
-
logoutRef.current = logout;
|
|
38
45
|
clearSessionStateRef.current = clearSessionState;
|
|
39
|
-
onRemoteSignOutRef.current = onRemoteSignOut;
|
|
40
|
-
onSessionRemovedRef.current = onSessionRemoved;
|
|
46
|
+
onRemoteSignOutRef.current = options?.onRemoteSignOut;
|
|
47
|
+
onSessionRemovedRef.current = options?.onSessionRemoved;
|
|
41
48
|
activeSessionIdRef.current = activeSessionId;
|
|
42
49
|
currentDeviceIdRef.current = currentDeviceId;
|
|
43
|
-
|
|
50
|
+
queryClientRef.current = queryClient;
|
|
51
|
+
}, [clearSessionState, options?.onRemoteSignOut, options?.onSessionRemoved, activeSessionId, currentDeviceId, queryClient]);
|
|
44
52
|
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
if (!userId || !baseURL) {
|
|
47
55
|
// Clean up if userId or baseURL becomes invalid
|
|
48
|
-
if (socketRef.current
|
|
49
|
-
socketRef.current.
|
|
50
|
-
|
|
56
|
+
if (socketRef.current) {
|
|
57
|
+
socketRef.current.disconnect();
|
|
58
|
+
socketRef.current = null;
|
|
51
59
|
}
|
|
52
60
|
return;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
socketRef.current = io(baseURL, {
|
|
60
|
-
transports: ['websocket'],
|
|
61
|
-
});
|
|
63
|
+
// Disconnect previous socket if switching users
|
|
64
|
+
if (socketRef.current) {
|
|
65
|
+
socketRef.current.disconnect();
|
|
66
|
+
socketRef.current = null;
|
|
62
67
|
}
|
|
63
|
-
const socket = socketRef.current;
|
|
64
68
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
debug.log('Emitting join for room:', roomId);
|
|
76
|
-
}
|
|
69
|
+
// Connect with auth token; use callback so reconnections get a fresh token
|
|
70
|
+
socketRef.current = io(baseURL, {
|
|
71
|
+
transports: ['websocket'],
|
|
72
|
+
auth: (cb: (data: { token: string }) => void) => {
|
|
73
|
+
const token = oxyServices.getAccessToken();
|
|
74
|
+
cb({ token: token ?? '' });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const socket = socketRef.current;
|
|
77
78
|
|
|
78
|
-
//
|
|
79
|
+
// Server auto-joins the user to `user:<userId>` room on connection
|
|
79
80
|
const handleConnect = () => {
|
|
80
81
|
debug.log('Socket connected:', socket.id);
|
|
81
82
|
};
|
|
82
83
|
|
|
84
|
+
const refreshSessions = () => {
|
|
85
|
+
invalidateSessionQueries(queryClientRef.current);
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
};
|
|
88
|
+
|
|
83
89
|
const handleSessionUpdate = async (data: {
|
|
84
90
|
type: string;
|
|
85
91
|
sessionId?: string;
|
|
@@ -87,17 +93,17 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
87
93
|
sessionIds?: string[]
|
|
88
94
|
}) => {
|
|
89
95
|
debug.log('Received session_update:', data);
|
|
90
|
-
|
|
96
|
+
|
|
91
97
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
92
|
-
const
|
|
93
|
-
|
|
98
|
+
const deviceId = currentDeviceIdRef.current;
|
|
99
|
+
|
|
94
100
|
// Handle different event types
|
|
95
101
|
if (data.type === 'session_removed') {
|
|
96
102
|
// Track removed session
|
|
97
103
|
if (data.sessionId && onSessionRemovedRef.current) {
|
|
98
104
|
onSessionRemovedRef.current(data.sessionId);
|
|
99
105
|
}
|
|
100
|
-
|
|
106
|
+
|
|
101
107
|
// If the removed sessionId matches the current activeSessionId, immediately clear state
|
|
102
108
|
if (data.sessionId === currentActiveSessionId) {
|
|
103
109
|
if (onRemoteSignOutRef.current) {
|
|
@@ -105,8 +111,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
105
111
|
} else {
|
|
106
112
|
toast.info('You have been signed out remotely.');
|
|
107
113
|
}
|
|
108
|
-
// Use clearSessionState since session was already removed server-side
|
|
109
|
-
// Await to ensure storage cleanup completes before continuing
|
|
110
114
|
try {
|
|
111
115
|
await clearSessionStateRef.current();
|
|
112
116
|
} catch (error) {
|
|
@@ -115,13 +119,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
} else {
|
|
118
|
-
|
|
119
|
-
refreshSessionsRef.current().catch((error) => {
|
|
120
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
121
|
-
if (__DEV__) {
|
|
122
|
-
logger.debug('Failed to refresh sessions after session_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
122
|
+
refreshSessions();
|
|
125
123
|
}
|
|
126
124
|
} else if (data.type === 'device_removed') {
|
|
127
125
|
// Track all removed sessions from this device
|
|
@@ -130,16 +128,14 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
130
128
|
onSessionRemovedRef.current(sessionId);
|
|
131
129
|
}
|
|
132
130
|
}
|
|
133
|
-
|
|
131
|
+
|
|
134
132
|
// If the removed deviceId matches the current device, immediately clear state
|
|
135
|
-
if (data.deviceId && data.deviceId ===
|
|
133
|
+
if (data.deviceId && data.deviceId === deviceId) {
|
|
136
134
|
if (onRemoteSignOutRef.current) {
|
|
137
135
|
onRemoteSignOutRef.current();
|
|
138
136
|
} else {
|
|
139
137
|
toast.info('This device has been removed. You have been signed out.');
|
|
140
138
|
}
|
|
141
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
142
|
-
// Await to ensure storage cleanup completes before continuing
|
|
143
139
|
try {
|
|
144
140
|
await clearSessionStateRef.current();
|
|
145
141
|
} catch (error) {
|
|
@@ -148,13 +144,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
148
144
|
}
|
|
149
145
|
}
|
|
150
146
|
} else {
|
|
151
|
-
|
|
152
|
-
refreshSessionsRef.current().catch((error) => {
|
|
153
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
154
|
-
if (__DEV__) {
|
|
155
|
-
logger.debug('Failed to refresh sessions after device_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
147
|
+
refreshSessions();
|
|
158
148
|
}
|
|
159
149
|
} else if (data.type === 'sessions_removed') {
|
|
160
150
|
// Track all removed sessions
|
|
@@ -163,7 +153,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
163
153
|
onSessionRemovedRef.current(sessionId);
|
|
164
154
|
}
|
|
165
155
|
}
|
|
166
|
-
|
|
156
|
+
|
|
167
157
|
// If the current activeSessionId is in the removed sessionIds list, immediately clear state
|
|
168
158
|
if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
|
|
169
159
|
if (onRemoteSignOutRef.current) {
|
|
@@ -171,8 +161,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
171
161
|
} else {
|
|
172
162
|
toast.info('You have been signed out remotely.');
|
|
173
163
|
}
|
|
174
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
175
|
-
// Await to ensure storage cleanup completes before continuing
|
|
176
164
|
try {
|
|
177
165
|
await clearSessionStateRef.current();
|
|
178
166
|
} catch (error) {
|
|
@@ -181,23 +169,12 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
181
169
|
}
|
|
182
170
|
}
|
|
183
171
|
} else {
|
|
184
|
-
|
|
185
|
-
refreshSessionsRef.current().catch((error) => {
|
|
186
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
187
|
-
if (__DEV__) {
|
|
188
|
-
logger.debug('Failed to refresh sessions after sessions_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
172
|
+
refreshSessions();
|
|
191
173
|
}
|
|
192
174
|
} else {
|
|
193
|
-
// For other event types (e.g., session_created), refresh sessions
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (__DEV__) {
|
|
197
|
-
logger.debug('Failed to refresh sessions after session_update', { component: 'useSessionSocket' }, error as unknown);
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
175
|
+
// For other event types (e.g., session_created), refresh sessions
|
|
176
|
+
refreshSessions();
|
|
177
|
+
|
|
201
178
|
// If the current session was logged out (legacy behavior), handle it specially
|
|
202
179
|
if (data.sessionId === currentActiveSessionId) {
|
|
203
180
|
if (onRemoteSignOutRef.current) {
|
|
@@ -205,8 +182,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
205
182
|
} else {
|
|
206
183
|
toast.info('You have been signed out remotely.');
|
|
207
184
|
}
|
|
208
|
-
// Use clearSessionState since session was already removed server-side
|
|
209
|
-
// Await to ensure storage cleanup completes before continuing
|
|
210
185
|
try {
|
|
211
186
|
await clearSessionStateRef.current();
|
|
212
187
|
} catch (error) {
|
|
@@ -222,12 +197,8 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
222
197
|
return () => {
|
|
223
198
|
socket.off('connect', handleConnect);
|
|
224
199
|
socket.off('session_update', handleSessionUpdate);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (joinedRoomRef.current === roomId) {
|
|
228
|
-
socket.emit('leave', { userId: roomId });
|
|
229
|
-
joinedRoomRef.current = null;
|
|
230
|
-
}
|
|
200
|
+
socket.disconnect();
|
|
201
|
+
socketRef.current = null;
|
|
231
202
|
};
|
|
232
203
|
}, [userId, baseURL]); // Only depend on userId and baseURL - callbacks are in refs
|
|
233
|
-
}
|
|
204
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -90,6 +90,7 @@ export type {
|
|
|
90
90
|
|
|
91
91
|
// --- Custom Hooks ---
|
|
92
92
|
export { useSessionSocket } from './hooks/useSessionSocket';
|
|
93
|
+
export type { UseSessionSocketOptions } from './hooks/useSessionSocket';
|
|
93
94
|
export { useAssets, setOxyAssetInstance } from './hooks/useAssets';
|
|
94
95
|
export { useFileDownloadUrl, setOxyFileUrlInstance } from './hooks/useFileDownloadUrl';
|
|
95
96
|
export { useFollow, useFollowerCounts } from './hooks/useFollow';
|