@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({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }) {
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
- const joinedRoomRef = (0, react_1.useRef)(null);
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
- // Update refs when callbacks change
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
- }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
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 && joinedRoomRef.current) {
38
- socketRef.current.emit('leave', { userId: joinedRoomRef.current });
39
- joinedRoomRef.current = null;
50
+ if (socketRef.current) {
51
+ socketRef.current.disconnect();
52
+ socketRef.current = null;
40
53
  }
41
54
  return;
42
55
  }
43
- const roomId = `user:${userId}`;
44
- // Only create socket if it doesn't exist
45
- if (!socketRef.current) {
46
- socketRef.current = (0, socket_io_client_1.default)(baseURL, {
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
- // Only join if we haven't already joined this room
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 currentDeviceId = currentDeviceIdRef.current;
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
- // Otherwise, just refresh the sessions list (with error handling)
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 === currentDeviceId) {
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
- // Otherwise, refresh sessions and device list (with error handling)
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
- // Otherwise, refresh sessions list (with error handling)
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 (with error handling)
178
- refreshSessionsRef.current().catch((error) => {
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
- // Only leave on unmount if we're still in this room
209
- if (joinedRoomRef.current === roomId) {
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({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }) {
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
- const joinedRoomRef = useRef(null);
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
- // Update refs when callbacks change
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
- }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
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 && joinedRoomRef.current) {
32
- socketRef.current.emit('leave', { userId: joinedRoomRef.current });
33
- joinedRoomRef.current = null;
44
+ if (socketRef.current) {
45
+ socketRef.current.disconnect();
46
+ socketRef.current = null;
34
47
  }
35
48
  return;
36
49
  }
37
- const roomId = `user:${userId}`;
38
- // Only create socket if it doesn't exist
39
- if (!socketRef.current) {
40
- socketRef.current = io(baseURL, {
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
- // Only join if we haven't already joined this room
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 currentDeviceId = currentDeviceIdRef.current;
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
- // Otherwise, just refresh the sessions list (with error handling)
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 === currentDeviceId) {
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
- // Otherwise, refresh sessions and device list (with error handling)
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
- // Otherwise, refresh sessions list (with error handling)
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 (with error handling)
172
- refreshSessionsRef.current().catch((error) => {
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
- // Only leave on unmount if we're still in this room
203
- if (joinedRoomRef.current === roomId) {
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 UseSessionSocketProps {
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({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps): void;
13
- export {};
5
+ export declare function useSessionSocket(options?: UseSessionSocketOptions): void;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/auth",
3
- "version": "1.1.4",
3
+ "version": "2.0.0",
4
4
  "description": "OxyHQ Web Authentication SDK — headless auth with React hooks for Next.js, Vite, and web apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -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 UseSessionSocketProps {
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({ userId, activeSessionId, currentDeviceId, refreshSessions, logout, clearSessionState, baseURL, onRemoteSignOut, onSessionRemoved }: UseSessionSocketProps) {
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
- const joinedRoomRef = useRef<string | null>(null);
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 callbacks change
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
- }, [refreshSessions, logout, clearSessionState, onRemoteSignOut, onSessionRemoved, activeSessionId, currentDeviceId]);
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 && joinedRoomRef.current) {
49
- socketRef.current.emit('leave', { userId: joinedRoomRef.current });
50
- joinedRoomRef.current = null;
56
+ if (socketRef.current) {
57
+ socketRef.current.disconnect();
58
+ socketRef.current = null;
51
59
  }
52
60
  return;
53
61
  }
54
62
 
55
- const roomId = `user:${userId}`;
56
-
57
- // Only create socket if it doesn't exist
58
- if (!socketRef.current) {
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
- // Only join if we haven't already joined this room
66
- if (joinedRoomRef.current !== roomId) {
67
- // Leave previous room if switching users
68
- if (joinedRoomRef.current) {
69
- socket.emit('leave', { userId: joinedRoomRef.current });
70
- }
71
-
72
- socket.emit('join', { userId: roomId });
73
- joinedRoomRef.current = roomId;
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
- // Set up event handlers (only once per socket instance)
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 currentDeviceId = currentDeviceIdRef.current;
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
- // Otherwise, just refresh the sessions list (with error handling)
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 === currentDeviceId) {
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
- // Otherwise, refresh sessions and device list (with error handling)
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
- // Otherwise, refresh sessions list (with error handling)
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 (with error handling)
194
- refreshSessionsRef.current().catch((error) => {
195
- // Log but don't throw - refresh errors shouldn't break the socket handler
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
- // Only leave on unmount if we're still in this room
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';