@oxyhq/auth 1.2.0 → 2.0.1
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 +36 -0
- package/dist/cjs/hooks/useSessionSocket.js +38 -53
- package/dist/esm/hooks/useSessionSocket.js +39 -54
- package/dist/types/hooks/useSessionSocket.d.ts +2 -11
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/hooks/useSessionSocket.ts +50 -70
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ npm install @oxyhq/auth
|
|
|
28
28
|
- **Query hooks** — useCurrentUser, useUserProfile, usePrivacySettings, useSecurityActivity, and more
|
|
29
29
|
- **Mutation hooks** — useUpdateProfile, useUploadAvatar, useSwitchSession, useLogoutSession, and more
|
|
30
30
|
- **Stores** — authStore, assetStore, accountStore, followStore (zustand)
|
|
31
|
+
- **useSessionSocket** — zero-config real-time session sync via WebSocket
|
|
31
32
|
- **Session management utilities**
|
|
32
33
|
|
|
33
34
|
## Usage
|
|
@@ -54,3 +55,38 @@ function YourApp() {
|
|
|
54
55
|
return <p>Welcome, {user?.name}</p>;
|
|
55
56
|
}
|
|
56
57
|
```
|
|
58
|
+
|
|
59
|
+
## Real-time Session Sync
|
|
60
|
+
|
|
61
|
+
`useSessionSocket` connects a WebSocket to the API and listens for session events (remote sign-out, device removal, etc.). It requires **zero configuration** — all auth state is pulled from `WebOxyProvider` context automatically.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { useSessionSocket } from '@oxyhq/auth';
|
|
65
|
+
|
|
66
|
+
function App() {
|
|
67
|
+
// Zero-config — just call it
|
|
68
|
+
useSessionSocket();
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Optional callbacks for custom handling:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
useSessionSocket({
|
|
76
|
+
onRemoteSignOut: () => router.push('/login'),
|
|
77
|
+
onSessionRemoved: (sessionId) => console.log('Session removed:', sessionId),
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Migration from v1.x
|
|
82
|
+
|
|
83
|
+
v1.x required passing 8+ props manually. In v2.0 all state is derived from context:
|
|
84
|
+
|
|
85
|
+
```diff
|
|
86
|
+
- useSessionSocket({
|
|
87
|
+
- userId, activeSessionId, currentDeviceId,
|
|
88
|
+
- refreshSessions, logout, clearSessionState,
|
|
89
|
+
- baseURL, getAccessToken,
|
|
90
|
+
- });
|
|
91
|
+
+ useSessionSocket();
|
|
92
|
+
```
|
|
@@ -9,29 +9,41 @@ 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
|
-
// Store callbacks in refs to avoid reconnecting when they change
|
|
16
|
-
const refreshSessionsRef = (0, react_1.useRef)(refreshSessions);
|
|
17
|
-
const logoutRef = (0, react_1.useRef)(logout);
|
|
31
|
+
// Store callbacks and values in refs to avoid reconnecting when they change
|
|
18
32
|
const clearSessionStateRef = (0, react_1.useRef)(clearSessionState);
|
|
19
|
-
const onRemoteSignOutRef = (0, react_1.useRef)(onRemoteSignOut);
|
|
20
|
-
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);
|
|
21
35
|
const activeSessionIdRef = (0, react_1.useRef)(activeSessionId);
|
|
22
36
|
const currentDeviceIdRef = (0, react_1.useRef)(currentDeviceId);
|
|
23
|
-
const
|
|
24
|
-
// Update refs when
|
|
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
|
-
|
|
34
|
-
}, [
|
|
45
|
+
queryClientRef.current = queryClient;
|
|
46
|
+
}, [clearSessionState, options?.onRemoteSignOut, options?.onSessionRemoved, activeSessionId, currentDeviceId, queryClient]);
|
|
35
47
|
(0, react_1.useEffect)(() => {
|
|
36
48
|
if (!userId || !baseURL) {
|
|
37
49
|
// Clean up if userId or baseURL becomes invalid
|
|
@@ -50,7 +62,7 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
50
62
|
socketRef.current = (0, socket_io_client_1.default)(baseURL, {
|
|
51
63
|
transports: ['websocket'],
|
|
52
64
|
auth: (cb) => {
|
|
53
|
-
const token =
|
|
65
|
+
const token = oxyServices.getAccessToken();
|
|
54
66
|
cb({ token: token ?? '' });
|
|
55
67
|
},
|
|
56
68
|
});
|
|
@@ -59,10 +71,14 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
59
71
|
const handleConnect = () => {
|
|
60
72
|
debug.log('Socket connected:', socket.id);
|
|
61
73
|
};
|
|
74
|
+
const refreshSessions = () => {
|
|
75
|
+
(0, queryKeys_1.invalidateSessionQueries)(queryClientRef.current);
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
};
|
|
62
78
|
const handleSessionUpdate = async (data) => {
|
|
63
79
|
debug.log('Received session_update:', data);
|
|
64
80
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
65
|
-
const
|
|
81
|
+
const deviceId = currentDeviceIdRef.current;
|
|
66
82
|
// Handle different event types
|
|
67
83
|
if (data.type === 'session_removed') {
|
|
68
84
|
// Track removed session
|
|
@@ -77,8 +93,6 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
77
93
|
else {
|
|
78
94
|
sonner_1.toast.info('You have been signed out remotely.');
|
|
79
95
|
}
|
|
80
|
-
// Use clearSessionState since session was already removed server-side
|
|
81
|
-
// Await to ensure storage cleanup completes before continuing
|
|
82
96
|
try {
|
|
83
97
|
await clearSessionStateRef.current();
|
|
84
98
|
}
|
|
@@ -89,13 +103,7 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
else {
|
|
92
|
-
|
|
93
|
-
refreshSessionsRef.current().catch((error) => {
|
|
94
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
95
|
-
if (__DEV__) {
|
|
96
|
-
core_1.logger.debug('Failed to refresh sessions after session_removed', { component: 'useSessionSocket' }, error);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
106
|
+
refreshSessions();
|
|
99
107
|
}
|
|
100
108
|
}
|
|
101
109
|
else if (data.type === 'device_removed') {
|
|
@@ -106,15 +114,13 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
// If the removed deviceId matches the current device, immediately clear state
|
|
109
|
-
if (data.deviceId && data.deviceId ===
|
|
117
|
+
if (data.deviceId && data.deviceId === deviceId) {
|
|
110
118
|
if (onRemoteSignOutRef.current) {
|
|
111
119
|
onRemoteSignOutRef.current();
|
|
112
120
|
}
|
|
113
121
|
else {
|
|
114
122
|
sonner_1.toast.info('This device has been removed. You have been signed out.');
|
|
115
123
|
}
|
|
116
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
117
|
-
// Await to ensure storage cleanup completes before continuing
|
|
118
124
|
try {
|
|
119
125
|
await clearSessionStateRef.current();
|
|
120
126
|
}
|
|
@@ -125,13 +131,7 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
else {
|
|
128
|
-
|
|
129
|
-
refreshSessionsRef.current().catch((error) => {
|
|
130
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
131
|
-
if (__DEV__) {
|
|
132
|
-
core_1.logger.debug('Failed to refresh sessions after device_removed', { component: 'useSessionSocket' }, error);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
134
|
+
refreshSessions();
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
else if (data.type === 'sessions_removed') {
|
|
@@ -149,8 +149,6 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
149
149
|
else {
|
|
150
150
|
sonner_1.toast.info('You have been signed out remotely.');
|
|
151
151
|
}
|
|
152
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
153
|
-
// Await to ensure storage cleanup completes before continuing
|
|
154
152
|
try {
|
|
155
153
|
await clearSessionStateRef.current();
|
|
156
154
|
}
|
|
@@ -161,23 +159,12 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
161
159
|
}
|
|
162
160
|
}
|
|
163
161
|
else {
|
|
164
|
-
|
|
165
|
-
refreshSessionsRef.current().catch((error) => {
|
|
166
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
167
|
-
if (__DEV__) {
|
|
168
|
-
core_1.logger.debug('Failed to refresh sessions after sessions_removed', { component: 'useSessionSocket' }, error);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
162
|
+
refreshSessions();
|
|
171
163
|
}
|
|
172
164
|
}
|
|
173
165
|
else {
|
|
174
|
-
// For other event types (e.g., session_created), refresh sessions
|
|
175
|
-
|
|
176
|
-
// Log but don't throw - refresh errors shouldn't break the socket handler
|
|
177
|
-
if (__DEV__) {
|
|
178
|
-
core_1.logger.debug('Failed to refresh sessions after session_update', { component: 'useSessionSocket' }, error);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
166
|
+
// For other event types (e.g., session_created), refresh sessions
|
|
167
|
+
refreshSessions();
|
|
181
168
|
// If the current session was logged out (legacy behavior), handle it specially
|
|
182
169
|
if (data.sessionId === currentActiveSessionId) {
|
|
183
170
|
if (onRemoteSignOutRef.current) {
|
|
@@ -186,8 +173,6 @@ function useSessionSocket({ userId, activeSessionId, currentDeviceId, refreshSes
|
|
|
186
173
|
else {
|
|
187
174
|
sonner_1.toast.info('You have been signed out remotely.');
|
|
188
175
|
}
|
|
189
|
-
// Use clearSessionState since session was already removed server-side
|
|
190
|
-
// Await to ensure storage cleanup completes before continuing
|
|
191
176
|
try {
|
|
192
177
|
await clearSessionStateRef.current();
|
|
193
178
|
}
|
|
@@ -1,31 +1,43 @@
|
|
|
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
|
-
// Store callbacks in refs to avoid reconnecting when they change
|
|
10
|
-
const refreshSessionsRef = useRef(refreshSessions);
|
|
11
|
-
const logoutRef = useRef(logout);
|
|
25
|
+
// Store callbacks and values in refs to avoid reconnecting when they change
|
|
12
26
|
const clearSessionStateRef = useRef(clearSessionState);
|
|
13
|
-
const onRemoteSignOutRef = useRef(onRemoteSignOut);
|
|
14
|
-
const onSessionRemovedRef = useRef(onSessionRemoved);
|
|
27
|
+
const onRemoteSignOutRef = useRef(options?.onRemoteSignOut);
|
|
28
|
+
const onSessionRemovedRef = useRef(options?.onSessionRemoved);
|
|
15
29
|
const activeSessionIdRef = useRef(activeSessionId);
|
|
16
30
|
const currentDeviceIdRef = useRef(currentDeviceId);
|
|
17
|
-
const
|
|
18
|
-
// Update refs when
|
|
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
|
-
|
|
28
|
-
}, [
|
|
39
|
+
queryClientRef.current = queryClient;
|
|
40
|
+
}, [clearSessionState, options?.onRemoteSignOut, options?.onSessionRemoved, activeSessionId, currentDeviceId, queryClient]);
|
|
29
41
|
useEffect(() => {
|
|
30
42
|
if (!userId || !baseURL) {
|
|
31
43
|
// Clean up if userId or baseURL becomes invalid
|
|
@@ -44,7 +56,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
44
56
|
socketRef.current = io(baseURL, {
|
|
45
57
|
transports: ['websocket'],
|
|
46
58
|
auth: (cb) => {
|
|
47
|
-
const token =
|
|
59
|
+
const token = oxyServices.getAccessToken();
|
|
48
60
|
cb({ token: token ?? '' });
|
|
49
61
|
},
|
|
50
62
|
});
|
|
@@ -53,10 +65,14 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
53
65
|
const handleConnect = () => {
|
|
54
66
|
debug.log('Socket connected:', socket.id);
|
|
55
67
|
};
|
|
68
|
+
const refreshSessions = () => {
|
|
69
|
+
invalidateSessionQueries(queryClientRef.current);
|
|
70
|
+
return Promise.resolve();
|
|
71
|
+
};
|
|
56
72
|
const handleSessionUpdate = async (data) => {
|
|
57
73
|
debug.log('Received session_update:', data);
|
|
58
74
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
59
|
-
const
|
|
75
|
+
const deviceId = currentDeviceIdRef.current;
|
|
60
76
|
// Handle different event types
|
|
61
77
|
if (data.type === 'session_removed') {
|
|
62
78
|
// Track removed session
|
|
@@ -71,8 +87,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
71
87
|
else {
|
|
72
88
|
toast.info('You have been signed out remotely.');
|
|
73
89
|
}
|
|
74
|
-
// Use clearSessionState since session was already removed server-side
|
|
75
|
-
// Await to ensure storage cleanup completes before continuing
|
|
76
90
|
try {
|
|
77
91
|
await clearSessionStateRef.current();
|
|
78
92
|
}
|
|
@@ -83,13 +97,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
99
|
else {
|
|
86
|
-
|
|
87
|
-
refreshSessionsRef.current().catch((error) => {
|
|
88
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
89
|
-
if (__DEV__) {
|
|
90
|
-
logger.debug('Failed to refresh sessions after session_removed', { component: 'useSessionSocket' }, error);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
100
|
+
refreshSessions();
|
|
93
101
|
}
|
|
94
102
|
}
|
|
95
103
|
else if (data.type === 'device_removed') {
|
|
@@ -100,15 +108,13 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
100
108
|
}
|
|
101
109
|
}
|
|
102
110
|
// If the removed deviceId matches the current device, immediately clear state
|
|
103
|
-
if (data.deviceId && data.deviceId ===
|
|
111
|
+
if (data.deviceId && data.deviceId === deviceId) {
|
|
104
112
|
if (onRemoteSignOutRef.current) {
|
|
105
113
|
onRemoteSignOutRef.current();
|
|
106
114
|
}
|
|
107
115
|
else {
|
|
108
116
|
toast.info('This device has been removed. You have been signed out.');
|
|
109
117
|
}
|
|
110
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
111
|
-
// Await to ensure storage cleanup completes before continuing
|
|
112
118
|
try {
|
|
113
119
|
await clearSessionStateRef.current();
|
|
114
120
|
}
|
|
@@ -119,13 +125,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
else {
|
|
122
|
-
|
|
123
|
-
refreshSessionsRef.current().catch((error) => {
|
|
124
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
125
|
-
if (__DEV__) {
|
|
126
|
-
logger.debug('Failed to refresh sessions after device_removed', { component: 'useSessionSocket' }, error);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
128
|
+
refreshSessions();
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
else if (data.type === 'sessions_removed') {
|
|
@@ -143,8 +143,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
143
143
|
else {
|
|
144
144
|
toast.info('You have been signed out remotely.');
|
|
145
145
|
}
|
|
146
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
147
|
-
// Await to ensure storage cleanup completes before continuing
|
|
148
146
|
try {
|
|
149
147
|
await clearSessionStateRef.current();
|
|
150
148
|
}
|
|
@@ -155,23 +153,12 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
155
153
|
}
|
|
156
154
|
}
|
|
157
155
|
else {
|
|
158
|
-
|
|
159
|
-
refreshSessionsRef.current().catch((error) => {
|
|
160
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
161
|
-
if (__DEV__) {
|
|
162
|
-
logger.debug('Failed to refresh sessions after sessions_removed', { component: 'useSessionSocket' }, error);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
156
|
+
refreshSessions();
|
|
165
157
|
}
|
|
166
158
|
}
|
|
167
159
|
else {
|
|
168
|
-
// For other event types (e.g., session_created), refresh sessions
|
|
169
|
-
|
|
170
|
-
// Log but don't throw - refresh errors shouldn't break the socket handler
|
|
171
|
-
if (__DEV__) {
|
|
172
|
-
logger.debug('Failed to refresh sessions after session_update', { component: 'useSessionSocket' }, error);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
160
|
+
// For other event types (e.g., session_created), refresh sessions
|
|
161
|
+
refreshSessions();
|
|
175
162
|
// If the current session was logged out (legacy behavior), handle it specially
|
|
176
163
|
if (data.sessionId === currentActiveSessionId) {
|
|
177
164
|
if (onRemoteSignOutRef.current) {
|
|
@@ -180,8 +167,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
180
167
|
else {
|
|
181
168
|
toast.info('You have been signed out remotely.');
|
|
182
169
|
}
|
|
183
|
-
// Use clearSessionState since session was already removed server-side
|
|
184
|
-
// Await to ensure storage cleanup completes before continuing
|
|
185
170
|
try {
|
|
186
171
|
await clearSessionStateRef.current();
|
|
187
172
|
}
|
|
@@ -1,14 +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;
|
|
9
|
-
getAccessToken: () => string | null;
|
|
1
|
+
export interface UseSessionSocketOptions {
|
|
10
2
|
onRemoteSignOut?: () => void;
|
|
11
3
|
onSessionRemoved?: (sessionId: string) => void;
|
|
12
4
|
}
|
|
13
|
-
export declare function useSessionSocket(
|
|
14
|
-
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,48 +1,54 @@
|
|
|
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;
|
|
17
|
-
getAccessToken: () => string | null;
|
|
13
|
+
export interface UseSessionSocketOptions {
|
|
18
14
|
onRemoteSignOut?: () => void;
|
|
19
15
|
onSessionRemoved?: (sessionId: string) => void;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
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
|
+
|
|
23
33
|
const socketRef = useRef<any>(null);
|
|
24
34
|
|
|
25
|
-
// Store callbacks in refs to avoid reconnecting when they change
|
|
26
|
-
const refreshSessionsRef = useRef(refreshSessions);
|
|
27
|
-
const logoutRef = useRef(logout);
|
|
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);
|
|
33
|
-
const
|
|
41
|
+
const queryClientRef = useRef(queryClient);
|
|
34
42
|
|
|
35
|
-
// Update refs when
|
|
43
|
+
// Update refs when values change
|
|
36
44
|
useEffect(() => {
|
|
37
|
-
refreshSessionsRef.current = refreshSessions;
|
|
38
|
-
logoutRef.current = logout;
|
|
39
45
|
clearSessionStateRef.current = clearSessionState;
|
|
40
|
-
onRemoteSignOutRef.current = onRemoteSignOut;
|
|
41
|
-
onSessionRemovedRef.current = onSessionRemoved;
|
|
46
|
+
onRemoteSignOutRef.current = options?.onRemoteSignOut;
|
|
47
|
+
onSessionRemovedRef.current = options?.onSessionRemoved;
|
|
42
48
|
activeSessionIdRef.current = activeSessionId;
|
|
43
49
|
currentDeviceIdRef.current = currentDeviceId;
|
|
44
|
-
|
|
45
|
-
}, [
|
|
50
|
+
queryClientRef.current = queryClient;
|
|
51
|
+
}, [clearSessionState, options?.onRemoteSignOut, options?.onSessionRemoved, activeSessionId, currentDeviceId, queryClient]);
|
|
46
52
|
|
|
47
53
|
useEffect(() => {
|
|
48
54
|
if (!userId || !baseURL) {
|
|
@@ -64,7 +70,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
64
70
|
socketRef.current = io(baseURL, {
|
|
65
71
|
transports: ['websocket'],
|
|
66
72
|
auth: (cb: (data: { token: string }) => void) => {
|
|
67
|
-
const token =
|
|
73
|
+
const token = oxyServices.getAccessToken();
|
|
68
74
|
cb({ token: token ?? '' });
|
|
69
75
|
},
|
|
70
76
|
});
|
|
@@ -75,6 +81,11 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
75
81
|
debug.log('Socket connected:', socket.id);
|
|
76
82
|
};
|
|
77
83
|
|
|
84
|
+
const refreshSessions = () => {
|
|
85
|
+
invalidateSessionQueries(queryClientRef.current);
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
};
|
|
88
|
+
|
|
78
89
|
const handleSessionUpdate = async (data: {
|
|
79
90
|
type: string;
|
|
80
91
|
sessionId?: string;
|
|
@@ -82,17 +93,17 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
82
93
|
sessionIds?: string[]
|
|
83
94
|
}) => {
|
|
84
95
|
debug.log('Received session_update:', data);
|
|
85
|
-
|
|
96
|
+
|
|
86
97
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
87
|
-
const
|
|
88
|
-
|
|
98
|
+
const deviceId = currentDeviceIdRef.current;
|
|
99
|
+
|
|
89
100
|
// Handle different event types
|
|
90
101
|
if (data.type === 'session_removed') {
|
|
91
102
|
// Track removed session
|
|
92
103
|
if (data.sessionId && onSessionRemovedRef.current) {
|
|
93
104
|
onSessionRemovedRef.current(data.sessionId);
|
|
94
105
|
}
|
|
95
|
-
|
|
106
|
+
|
|
96
107
|
// If the removed sessionId matches the current activeSessionId, immediately clear state
|
|
97
108
|
if (data.sessionId === currentActiveSessionId) {
|
|
98
109
|
if (onRemoteSignOutRef.current) {
|
|
@@ -100,8 +111,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
100
111
|
} else {
|
|
101
112
|
toast.info('You have been signed out remotely.');
|
|
102
113
|
}
|
|
103
|
-
// Use clearSessionState since session was already removed server-side
|
|
104
|
-
// Await to ensure storage cleanup completes before continuing
|
|
105
114
|
try {
|
|
106
115
|
await clearSessionStateRef.current();
|
|
107
116
|
} catch (error) {
|
|
@@ -110,13 +119,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
121
|
} else {
|
|
113
|
-
|
|
114
|
-
refreshSessionsRef.current().catch((error) => {
|
|
115
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
116
|
-
if (__DEV__) {
|
|
117
|
-
logger.debug('Failed to refresh sessions after session_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
122
|
+
refreshSessions();
|
|
120
123
|
}
|
|
121
124
|
} else if (data.type === 'device_removed') {
|
|
122
125
|
// Track all removed sessions from this device
|
|
@@ -125,16 +128,14 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
125
128
|
onSessionRemovedRef.current(sessionId);
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
|
-
|
|
131
|
+
|
|
129
132
|
// If the removed deviceId matches the current device, immediately clear state
|
|
130
|
-
if (data.deviceId && data.deviceId ===
|
|
133
|
+
if (data.deviceId && data.deviceId === deviceId) {
|
|
131
134
|
if (onRemoteSignOutRef.current) {
|
|
132
135
|
onRemoteSignOutRef.current();
|
|
133
136
|
} else {
|
|
134
137
|
toast.info('This device has been removed. You have been signed out.');
|
|
135
138
|
}
|
|
136
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
137
|
-
// Await to ensure storage cleanup completes before continuing
|
|
138
139
|
try {
|
|
139
140
|
await clearSessionStateRef.current();
|
|
140
141
|
} catch (error) {
|
|
@@ -143,13 +144,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
} else {
|
|
146
|
-
|
|
147
|
-
refreshSessionsRef.current().catch((error) => {
|
|
148
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
149
|
-
if (__DEV__) {
|
|
150
|
-
logger.debug('Failed to refresh sessions after device_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
147
|
+
refreshSessions();
|
|
153
148
|
}
|
|
154
149
|
} else if (data.type === 'sessions_removed') {
|
|
155
150
|
// Track all removed sessions
|
|
@@ -158,7 +153,7 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
158
153
|
onSessionRemovedRef.current(sessionId);
|
|
159
154
|
}
|
|
160
155
|
}
|
|
161
|
-
|
|
156
|
+
|
|
162
157
|
// If the current activeSessionId is in the removed sessionIds list, immediately clear state
|
|
163
158
|
if (data.sessionIds && currentActiveSessionId && data.sessionIds.includes(currentActiveSessionId)) {
|
|
164
159
|
if (onRemoteSignOutRef.current) {
|
|
@@ -166,8 +161,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
166
161
|
} else {
|
|
167
162
|
toast.info('You have been signed out remotely.');
|
|
168
163
|
}
|
|
169
|
-
// Use clearSessionState since sessions were already removed server-side
|
|
170
|
-
// Await to ensure storage cleanup completes before continuing
|
|
171
164
|
try {
|
|
172
165
|
await clearSessionStateRef.current();
|
|
173
166
|
} catch (error) {
|
|
@@ -176,23 +169,12 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
176
169
|
}
|
|
177
170
|
}
|
|
178
171
|
} else {
|
|
179
|
-
|
|
180
|
-
refreshSessionsRef.current().catch((error) => {
|
|
181
|
-
// Silently handle errors from refresh - they're expected if sessions were removed
|
|
182
|
-
if (__DEV__) {
|
|
183
|
-
logger.debug('Failed to refresh sessions after sessions_removed', { component: 'useSessionSocket' }, error as unknown);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
172
|
+
refreshSessions();
|
|
186
173
|
}
|
|
187
174
|
} else {
|
|
188
|
-
// For other event types (e.g., session_created), refresh sessions
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (__DEV__) {
|
|
192
|
-
logger.debug('Failed to refresh sessions after session_update', { component: 'useSessionSocket' }, error as unknown);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
175
|
+
// For other event types (e.g., session_created), refresh sessions
|
|
176
|
+
refreshSessions();
|
|
177
|
+
|
|
196
178
|
// If the current session was logged out (legacy behavior), handle it specially
|
|
197
179
|
if (data.sessionId === currentActiveSessionId) {
|
|
198
180
|
if (onRemoteSignOutRef.current) {
|
|
@@ -200,8 +182,6 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
200
182
|
} else {
|
|
201
183
|
toast.info('You have been signed out remotely.');
|
|
202
184
|
}
|
|
203
|
-
// Use clearSessionState since session was already removed server-side
|
|
204
|
-
// Await to ensure storage cleanup completes before continuing
|
|
205
185
|
try {
|
|
206
186
|
await clearSessionStateRef.current();
|
|
207
187
|
} catch (error) {
|
|
@@ -221,4 +201,4 @@ export function useSessionSocket({ userId, activeSessionId, currentDeviceId, ref
|
|
|
221
201
|
socketRef.current = null;
|
|
222
202
|
};
|
|
223
203
|
}, [userId, baseURL]); // Only depend on userId and baseURL - callbacks are in refs
|
|
224
|
-
}
|
|
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';
|