@terreno/rtk 0.0.9
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 +97 -0
- package/dist/authSlice.d.ts +73 -0
- package/dist/authSlice.d.ts.map +1 -0
- package/dist/authSlice.js +227 -0
- package/dist/authSlice.js.map +1 -0
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +76 -0
- package/dist/constants.js.map +1 -0
- package/dist/emptyApi.d.ts +19 -0
- package/dist/emptyApi.d.ts.map +1 -0
- package/dist/emptyApi.js +297 -0
- package/dist/emptyApi.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mongooseSlice.d.ts +11 -0
- package/dist/mongooseSlice.d.ts.map +1 -0
- package/dist/mongooseSlice.js +9 -0
- package/dist/mongooseSlice.js.map +1 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +3 -0
- package/dist/platform.js.map +1 -0
- package/dist/socket.d.ts +20 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/socket.js +317 -0
- package/dist/socket.js.map +1 -0
- package/dist/tagGenerator.d.ts +2 -0
- package/dist/tagGenerator.d.ts.map +1 -0
- package/dist/tagGenerator.js +60 -0
- package/dist/tagGenerator.js.map +1 -0
- package/package.json +47 -0
- package/src/authSlice.ts +274 -0
- package/src/constants.ts +105 -0
- package/src/emptyApi.ts +335 -0
- package/src/index.ts +7 -0
- package/src/mongooseSlice.ts +19 -0
- package/src/platform.ts +3 -0
- package/src/socket.ts +436 -0
- package/src/tagGenerator.ts +82 -0
package/src/socket.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import {useToast} from "@terreno/ui";
|
|
2
|
+
import {DateTime} from "luxon";
|
|
3
|
+
import {useCallback, useEffect, useRef, useState} from "react";
|
|
4
|
+
import {useSelector} from "react-redux";
|
|
5
|
+
import {io, type Socket} from "socket.io-client";
|
|
6
|
+
import {selectLastTokenRefreshTimestamp} from "./authSlice";
|
|
7
|
+
import {logAuth} from "./constants";
|
|
8
|
+
import {getFriendlyExpirationInfo, getTokenExpirationTimes, refreshAuthToken} from "./emptyApi";
|
|
9
|
+
|
|
10
|
+
export interface SocketConnection {
|
|
11
|
+
isConnected: boolean;
|
|
12
|
+
lastDisconnectedAt: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseSocketConnectionOptions {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
onConnect?: () => void;
|
|
18
|
+
onDisconnect?: () => void;
|
|
19
|
+
onConnectError?: (error: Error) => void;
|
|
20
|
+
onReconnectFailed?: () => void;
|
|
21
|
+
getAuthToken: () => Promise<string | null>;
|
|
22
|
+
shouldConnect: boolean;
|
|
23
|
+
captureEvent?: (eventName: string, data: Record<string, unknown>) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const useSocketConnection = ({
|
|
27
|
+
baseUrl,
|
|
28
|
+
onConnect,
|
|
29
|
+
onDisconnect,
|
|
30
|
+
onConnectError,
|
|
31
|
+
onReconnectFailed,
|
|
32
|
+
getAuthToken,
|
|
33
|
+
shouldConnect, // Whether we have a logged in user.
|
|
34
|
+
captureEvent,
|
|
35
|
+
}: UseSocketConnectionOptions): {
|
|
36
|
+
socket: Socket | null;
|
|
37
|
+
isSocketConnected: SocketConnection;
|
|
38
|
+
} => {
|
|
39
|
+
const toast = useToast();
|
|
40
|
+
const [socket, setSocket] = useState<Socket | null>(null);
|
|
41
|
+
const isConnectedRef = useRef<SocketConnection>(undefined);
|
|
42
|
+
const [isSocketConnected, setIsSocketConnected] = useState<SocketConnection>({
|
|
43
|
+
isConnected: socket?.connected ?? false,
|
|
44
|
+
lastDisconnectedAt: null,
|
|
45
|
+
});
|
|
46
|
+
const disconnectedToastId = useRef<string | null>(null);
|
|
47
|
+
const tokenErrorToastId = useRef<string | null>(null);
|
|
48
|
+
|
|
49
|
+
// Keep ref updated with latest socket connection state
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
isConnectedRef.current = isSocketConnected;
|
|
52
|
+
}, [isSocketConnected]);
|
|
53
|
+
|
|
54
|
+
// Initialize socket connection
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const socketIo = io(baseUrl, {
|
|
57
|
+
autoConnect: false,
|
|
58
|
+
reconnection: true,
|
|
59
|
+
reconnectionAttempts: 5,
|
|
60
|
+
reconnectionDelay: 1000,
|
|
61
|
+
reconnectionDelayMax: 5000,
|
|
62
|
+
transports: ["websocket"],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
setSocket(socketIo);
|
|
66
|
+
|
|
67
|
+
return (): void => {
|
|
68
|
+
socketIo.disconnect();
|
|
69
|
+
};
|
|
70
|
+
}, [baseUrl]);
|
|
71
|
+
|
|
72
|
+
const hideDisconnectedToast = useCallback((): void => {
|
|
73
|
+
if (disconnectedToastId.current) {
|
|
74
|
+
toast.hide(disconnectedToastId.current);
|
|
75
|
+
disconnectedToastId.current = null;
|
|
76
|
+
}
|
|
77
|
+
}, [toast]);
|
|
78
|
+
|
|
79
|
+
const hideTokenErrorToast = useCallback((): void => {
|
|
80
|
+
if (tokenErrorToastId.current) {
|
|
81
|
+
toast.hide(tokenErrorToastId.current);
|
|
82
|
+
tokenErrorToastId.current = null;
|
|
83
|
+
}
|
|
84
|
+
}, [toast]);
|
|
85
|
+
|
|
86
|
+
// Connect the socket with the current auth token
|
|
87
|
+
const connectSocket = useCallback(async (): Promise<void> => {
|
|
88
|
+
const token = await getAuthToken();
|
|
89
|
+
|
|
90
|
+
if (!token) {
|
|
91
|
+
console.warn(
|
|
92
|
+
"[SocketConnection] Attempting to connect socket, but getAuthToken returned no token."
|
|
93
|
+
);
|
|
94
|
+
// Don't capture this event because it's expected when the user is logged out.
|
|
95
|
+
return;
|
|
96
|
+
} else {
|
|
97
|
+
logAuth("[SocketConnection] Token received from getAuthToken.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (socket) {
|
|
101
|
+
// Enhanced logging for Option 1 (token status)
|
|
102
|
+
logAuth(
|
|
103
|
+
`[SocketConnection] Socket connecting ${token ? "with" : "without"} token. Current socket state: ${socket.connected ? "connected" : "disconnected"}`
|
|
104
|
+
);
|
|
105
|
+
socket.auth = {token: `Bearer ${token}`};
|
|
106
|
+
socket.connect();
|
|
107
|
+
} else {
|
|
108
|
+
console.warn("[SocketConnection] connectSocket called but socket instance is null.");
|
|
109
|
+
}
|
|
110
|
+
}, [socket, getAuthToken]);
|
|
111
|
+
|
|
112
|
+
// Extracted logic for checking token expiration, refreshing token, and handling related UI
|
|
113
|
+
const checkAndRefreshTokenLogic = useCallback(
|
|
114
|
+
async (context: "disconnect" | "connect_error"): Promise<void> => {
|
|
115
|
+
let authRemainingSecs: number | undefined;
|
|
116
|
+
let refreshRemainingSecs: number | undefined;
|
|
117
|
+
try {
|
|
118
|
+
const expirationTimes = await getTokenExpirationTimes();
|
|
119
|
+
authRemainingSecs = expirationTimes.authRemainingSecs;
|
|
120
|
+
refreshRemainingSecs = expirationTimes.refreshRemainingSecs;
|
|
121
|
+
logAuth(
|
|
122
|
+
`[SocketConnection] Token status on ${context}: authRemainingSecs: ${authRemainingSecs}, refreshRemainingSecs: ${refreshRemainingSecs}`
|
|
123
|
+
);
|
|
124
|
+
if (
|
|
125
|
+
(authRemainingSecs !== undefined && authRemainingSecs < 60) ||
|
|
126
|
+
(refreshRemainingSecs !== undefined && refreshRemainingSecs < 60)
|
|
127
|
+
) {
|
|
128
|
+
logAuth(
|
|
129
|
+
`[SocketConnection] Auth or refresh token nearing expiration or expired on ${context}, attempting refresh.`
|
|
130
|
+
);
|
|
131
|
+
await refreshAuthToken();
|
|
132
|
+
// Attempt to reconnect after token refresh
|
|
133
|
+
if (shouldConnect && socket && !socket.connected) {
|
|
134
|
+
logAuth(
|
|
135
|
+
`[SocketConnection] Attempting to reconnect socket after token refresh due to ${context}.`
|
|
136
|
+
);
|
|
137
|
+
socket.connect();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const socketError = error as Error;
|
|
142
|
+
console.error(
|
|
143
|
+
`[SocketConnection] Error checking/refreshing token on ${context}:`,
|
|
144
|
+
socketError
|
|
145
|
+
);
|
|
146
|
+
if (refreshRemainingSecs !== undefined && refreshRemainingSecs > 0) {
|
|
147
|
+
const tokenInfo = await getFriendlyExpirationInfo();
|
|
148
|
+
// Only capture this event if the refresh token is still valid,
|
|
149
|
+
// otherwise it's expected it will fail.
|
|
150
|
+
captureEvent?.(
|
|
151
|
+
`WebSocket Token Check/Refresh Error on ${context === "disconnect" ? "Disconnect" : "ConnectError"}`,
|
|
152
|
+
{
|
|
153
|
+
authRemainingSecs,
|
|
154
|
+
error: socketError.message,
|
|
155
|
+
refreshRemainingSecs,
|
|
156
|
+
time: DateTime.now().toISO(),
|
|
157
|
+
tokenInfo,
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
hideDisconnectedToast();
|
|
162
|
+
if (!tokenErrorToastId.current) {
|
|
163
|
+
tokenErrorToastId.current = toast.show(
|
|
164
|
+
"Error refreshing token. Please log out and log back in if reconnections fail. Your work may not be saved if you continue.",
|
|
165
|
+
{
|
|
166
|
+
onDismiss: (): void => hideTokenErrorToast(),
|
|
167
|
+
persistent: true,
|
|
168
|
+
variant: "error",
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
[shouldConnect, socket, captureEvent, hideDisconnectedToast, toast, hideTokenErrorToast]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Use Redux state for token refresh signal
|
|
178
|
+
const lastTokenRefreshTimestamp = useSelector(selectLastTokenRefreshTimestamp);
|
|
179
|
+
const previousTokenRefreshTimestampRef = useRef<number | null>(null);
|
|
180
|
+
|
|
181
|
+
// Effect to handle token refresh events from Redux state
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (
|
|
184
|
+
lastTokenRefreshTimestamp &&
|
|
185
|
+
lastTokenRefreshTimestamp !== previousTokenRefreshTimestampRef.current
|
|
186
|
+
) {
|
|
187
|
+
if (tokenErrorToastId.current) {
|
|
188
|
+
logAuth(
|
|
189
|
+
"[SocketConnection] Token refresh detected via Redux state, dismissing error toast and attempting reconnect."
|
|
190
|
+
);
|
|
191
|
+
hideTokenErrorToast();
|
|
192
|
+
}
|
|
193
|
+
if (shouldConnect && !socket?.connected) {
|
|
194
|
+
logAuth(
|
|
195
|
+
"[SocketConnection] Attempting to connect socket after token refresh detected via Redux state."
|
|
196
|
+
);
|
|
197
|
+
void connectSocket();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
previousTokenRefreshTimestampRef.current = lastTokenRefreshTimestamp;
|
|
201
|
+
}, [lastTokenRefreshTimestamp, socket, shouldConnect, connectSocket, hideTokenErrorToast]);
|
|
202
|
+
|
|
203
|
+
// Connect/disconnect socket based on shouldConnect flag
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (shouldConnect) {
|
|
206
|
+
if (!isSocketConnected.isConnected) {
|
|
207
|
+
logAuth(
|
|
208
|
+
`[SocketConnection] Attempting to connect socket because shouldConnect is true and socket is not connected.`
|
|
209
|
+
);
|
|
210
|
+
void connectSocket();
|
|
211
|
+
} else {
|
|
212
|
+
logAuth(
|
|
213
|
+
`[SocketConnection] Socket is already connected and shouldConnect is true. No action needed.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
if (isSocketConnected.isConnected) {
|
|
218
|
+
logAuth(
|
|
219
|
+
`[SocketConnection] Attempting to disconnect socket because shouldConnect is false and socket is connected.`
|
|
220
|
+
);
|
|
221
|
+
socket?.disconnect();
|
|
222
|
+
setIsSocketConnected({
|
|
223
|
+
isConnected: false,
|
|
224
|
+
lastDisconnectedAt: null, // null because this was intentional
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
logAuth(
|
|
228
|
+
`[SocketConnection] Socket is already disconnected and shouldConnect is false. No action needed.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, [connectSocket, shouldConnect, isSocketConnected, socket]);
|
|
233
|
+
|
|
234
|
+
// Attempt to reconnect if token was refreshed and we are disconnected
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (shouldConnect && !isSocketConnected.isConnected && socket) {
|
|
237
|
+
logAuth("[SocketConnection] Token refresh detected, attempting to reconnect socket.");
|
|
238
|
+
// We might want to ensure the socket isn't already in a connecting state here
|
|
239
|
+
// if socket.io-client provides such a state.
|
|
240
|
+
// Forcing a disconnect first can help if it's stuck in a bad state.
|
|
241
|
+
socket.disconnect();
|
|
242
|
+
void connectSocket();
|
|
243
|
+
}
|
|
244
|
+
}, [shouldConnect, isSocketConnected.isConnected, socket, connectSocket]);
|
|
245
|
+
|
|
246
|
+
// Show toast when disconnected
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!shouldConnect) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const checkShowToast = async (): Promise<void> => {
|
|
253
|
+
// if there is an error toast, don't show the disconnect toast
|
|
254
|
+
if (tokenErrorToastId.current) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const shouldShowDisconnectToast =
|
|
258
|
+
!isConnectedRef.current?.isConnected &&
|
|
259
|
+
isConnectedRef.current?.lastDisconnectedAt &&
|
|
260
|
+
DateTime.now().diff(DateTime.fromISO(isConnectedRef.current.lastDisconnectedAt), "seconds")
|
|
261
|
+
.seconds > 9;
|
|
262
|
+
|
|
263
|
+
if (shouldShowDisconnectToast && !disconnectedToastId.current) {
|
|
264
|
+
disconnectedToastId.current = toast.show(
|
|
265
|
+
"You have been disconnected. Attempting to reconnect...",
|
|
266
|
+
{
|
|
267
|
+
onDismiss: (): void => hideDisconnectedToast(),
|
|
268
|
+
persistent: true,
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
} else if (!shouldShowDisconnectToast && disconnectedToastId.current) {
|
|
272
|
+
// If we should no longer show the toast but it is still showing, hide it
|
|
273
|
+
hideDisconnectedToast();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
278
|
+
|
|
279
|
+
// Check every second if we've reconnected
|
|
280
|
+
const startCheckingConnection = (): void => {
|
|
281
|
+
if (!isConnectedRef.current?.isConnected && !intervalId) {
|
|
282
|
+
intervalId = setInterval(async () => {
|
|
283
|
+
await checkShowToast();
|
|
284
|
+
if (isConnectedRef.current?.isConnected && intervalId) {
|
|
285
|
+
clearInterval(intervalId);
|
|
286
|
+
intervalId = null;
|
|
287
|
+
}
|
|
288
|
+
}, 1000);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
startCheckingConnection();
|
|
293
|
+
|
|
294
|
+
return (): void => {
|
|
295
|
+
if (intervalId) {
|
|
296
|
+
clearInterval(intervalId);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}, [hideDisconnectedToast, shouldConnect, toast]);
|
|
300
|
+
|
|
301
|
+
// Set up basic socket event listeners
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (!socket) return;
|
|
304
|
+
|
|
305
|
+
const handleConnect = (): void => {
|
|
306
|
+
logAuth("[SocketConnection] Socket connected");
|
|
307
|
+
hideDisconnectedToast();
|
|
308
|
+
hideTokenErrorToast();
|
|
309
|
+
|
|
310
|
+
// don't show toast if was disconnected and now connected within 10 seconds
|
|
311
|
+
if (
|
|
312
|
+
isSocketConnected.lastDisconnectedAt &&
|
|
313
|
+
DateTime.now().diff(DateTime.fromISO(isSocketConnected.lastDisconnectedAt), "seconds")
|
|
314
|
+
.seconds > 10
|
|
315
|
+
) {
|
|
316
|
+
toast.show("You have been reconnected.");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
setIsSocketConnected({
|
|
320
|
+
isConnected: true,
|
|
321
|
+
lastDisconnectedAt: null,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
onConnect?.();
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const handleDisconnect = async (reason: Socket.DisconnectReason): Promise<void> => {
|
|
328
|
+
const tokenInfo = await getFriendlyExpirationInfo();
|
|
329
|
+
|
|
330
|
+
// Enhanced logging for Option 1 (disconnect reason)
|
|
331
|
+
logAuth(
|
|
332
|
+
`[SocketConnection] Socket disconnected, reason: ${reason}, token status: ${tokenInfo}`
|
|
333
|
+
);
|
|
334
|
+
setIsSocketConnected({
|
|
335
|
+
isConnected: false,
|
|
336
|
+
lastDisconnectedAt: DateTime.now().toISO(),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
captureEvent?.("WebSocket Disconnection", {
|
|
340
|
+
reason,
|
|
341
|
+
time: DateTime.now().toISO(),
|
|
342
|
+
tokenInfo,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Check token status on disconnect
|
|
346
|
+
await checkAndRefreshTokenLogic("disconnect");
|
|
347
|
+
|
|
348
|
+
await onDisconnect?.();
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const handleConnectError = async (connectionError: Error): Promise<void> => {
|
|
352
|
+
const tokenInfo = await getFriendlyExpirationInfo();
|
|
353
|
+
|
|
354
|
+
console.error(
|
|
355
|
+
"[SocketConnection] Socket connection error:",
|
|
356
|
+
connectionError,
|
|
357
|
+
"Token status:",
|
|
358
|
+
tokenInfo
|
|
359
|
+
);
|
|
360
|
+
captureEvent?.("WebSocket Connection Error", {
|
|
361
|
+
error: connectionError.message,
|
|
362
|
+
time: DateTime.now().toISO(),
|
|
363
|
+
tokenInfo,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Check token status on connect_error
|
|
367
|
+
await checkAndRefreshTokenLogic("connect_error");
|
|
368
|
+
|
|
369
|
+
onConnectError?.(connectionError);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const handleReconnectFailed = async (): Promise<void> => {
|
|
373
|
+
const tokenInfo = await getFriendlyExpirationInfo();
|
|
374
|
+
|
|
375
|
+
console.error(
|
|
376
|
+
"[SocketConnection] Socket reconnection failed after exhausting reconnection attempts.",
|
|
377
|
+
"Token status:",
|
|
378
|
+
tokenInfo
|
|
379
|
+
);
|
|
380
|
+
captureEvent?.("WebSocket Reconnect Failed", {
|
|
381
|
+
time: DateTime.now().toISO(),
|
|
382
|
+
tokenInfo,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Force a new connection attempt
|
|
386
|
+
socket.disconnect();
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
// Check shouldConnect and if still disconnected using the ref for the most current state
|
|
389
|
+
if (shouldConnect && isConnectedRef.current && !isConnectedRef.current.isConnected) {
|
|
390
|
+
logAuth(
|
|
391
|
+
"[SocketConnection] Attempting to force a new connection after reconnect_failed event."
|
|
392
|
+
);
|
|
393
|
+
void connectSocket();
|
|
394
|
+
} else if (!shouldConnect) {
|
|
395
|
+
logAuth(
|
|
396
|
+
"[SocketConnection] Not attempting to reconnect after reconnect_failed because shouldConnect is false."
|
|
397
|
+
);
|
|
398
|
+
} else if (isConnectedRef.current?.isConnected) {
|
|
399
|
+
logAuth(
|
|
400
|
+
"[SocketConnection] Not attempting to reconnect after reconnect_failed because socket is now connected."
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}, 2000);
|
|
404
|
+
onReconnectFailed?.();
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Attach event listeners
|
|
408
|
+
socket.on("connect", handleConnect);
|
|
409
|
+
socket.on("disconnect", handleDisconnect);
|
|
410
|
+
socket.on("connect_error", handleConnectError);
|
|
411
|
+
socket.on("reconnect_failed", handleReconnectFailed);
|
|
412
|
+
|
|
413
|
+
return (): void => {
|
|
414
|
+
socket.off("connect", handleConnect);
|
|
415
|
+
socket.off("disconnect", handleDisconnect);
|
|
416
|
+
socket.off("connect_error", handleConnectError);
|
|
417
|
+
socket.off("reconnect_failed", handleReconnectFailed);
|
|
418
|
+
};
|
|
419
|
+
}, [
|
|
420
|
+
socket,
|
|
421
|
+
hideDisconnectedToast,
|
|
422
|
+
isSocketConnected.lastDisconnectedAt,
|
|
423
|
+
captureEvent,
|
|
424
|
+
onConnect,
|
|
425
|
+
onDisconnect,
|
|
426
|
+
onConnectError,
|
|
427
|
+
onReconnectFailed,
|
|
428
|
+
shouldConnect,
|
|
429
|
+
connectSocket,
|
|
430
|
+
toast,
|
|
431
|
+
hideTokenErrorToast,
|
|
432
|
+
checkAndRefreshTokenLogic,
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
return {isSocketConnected, socket};
|
|
436
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: Generics
|
|
2
|
+
|
|
3
|
+
// use this with enhanceEndpoints since the code generator doesn't invalidate by individual ids,
|
|
4
|
+
// only at the full collection level
|
|
5
|
+
|
|
6
|
+
const providesIdTags =
|
|
7
|
+
(path: string) =>
|
|
8
|
+
(result: any): string[] | [{type: string; id?: string}] =>
|
|
9
|
+
result ? [...(result?.data?.map(({_id}: any) => ({id: _id, type: path})) ?? []), path] : [path];
|
|
10
|
+
|
|
11
|
+
const providesIdTag =
|
|
12
|
+
(path: string) =>
|
|
13
|
+
(result: any): string[] | [{type: string; id?: string}] => {
|
|
14
|
+
return result ? [{id: result._id, type: path}] : [path];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const invalidatesIdTags =
|
|
18
|
+
(path: string) =>
|
|
19
|
+
(result: any): string[] | [{type: string; id?: string}] =>
|
|
20
|
+
result ? [...(result?.data?.map(({_id}: any) => ({id: _id, type: path})) ?? []), path] : [path];
|
|
21
|
+
|
|
22
|
+
const cleanEndpointStringToGenerateTag = (string: string): string => {
|
|
23
|
+
// Define the prefixes and suffix
|
|
24
|
+
const prefixes = ["patch", "get", "delete"];
|
|
25
|
+
const suffix = "ById";
|
|
26
|
+
|
|
27
|
+
// Create a regular expression to match the prefixes and suffix
|
|
28
|
+
const prefixPattern = `^(${prefixes.join("|")})`;
|
|
29
|
+
const suffixPattern = `${suffix}$`;
|
|
30
|
+
const regex = new RegExp(`${prefixPattern}|${suffixPattern}`, "gi");
|
|
31
|
+
|
|
32
|
+
// Replace the matched parts and convert to lowercase
|
|
33
|
+
return string.replace(regex, "")?.toLowerCase();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const generateTags = (api: any, tagTypes: string[]): any => {
|
|
37
|
+
// take the api, and for each get and list endpoint, generate tags that invalidate the cache by id
|
|
38
|
+
// and by the list endpoint
|
|
39
|
+
const endpoints = api.endpoints;
|
|
40
|
+
const tags: any = {};
|
|
41
|
+
Object.keys(endpoints).forEach((endpoint) => {
|
|
42
|
+
if (endpoint === "getConversations") {
|
|
43
|
+
tags[endpoint] = {invalidatesTags: ["conversations", "messages"]};
|
|
44
|
+
}
|
|
45
|
+
if (endpoint.toLowerCase().includes("get")) {
|
|
46
|
+
// List endpoints
|
|
47
|
+
if (!endpoint.toLowerCase().includes("byid")) {
|
|
48
|
+
const tag = tagTypes.find((t: string) =>
|
|
49
|
+
// remove "get" from the endpoint name and "ById" from the endpoint name
|
|
50
|
+
t
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.includes(cleanEndpointStringToGenerateTag(endpoint))
|
|
53
|
+
);
|
|
54
|
+
if (tag) {
|
|
55
|
+
tags[endpoint] = {providesTags: providesIdTags(tag)};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Read endpoints
|
|
59
|
+
else {
|
|
60
|
+
const tag = tagTypes.find((t: string) =>
|
|
61
|
+
t.toLowerCase().includes(cleanEndpointStringToGenerateTag(endpoint))
|
|
62
|
+
);
|
|
63
|
+
if (tag) {
|
|
64
|
+
tags[endpoint] = {providesTags: providesIdTag(tag)};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Patch and delete endpoints
|
|
69
|
+
else if (
|
|
70
|
+
endpoint.toLowerCase().includes("patch") ||
|
|
71
|
+
endpoint.toLowerCase().includes("delete")
|
|
72
|
+
) {
|
|
73
|
+
const tag = tagTypes.find((t: string) =>
|
|
74
|
+
t.toLowerCase().includes(cleanEndpointStringToGenerateTag(endpoint))
|
|
75
|
+
);
|
|
76
|
+
if (tag) {
|
|
77
|
+
tags[endpoint] = {invalidatesTags: invalidatesIdTags(tag)};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return tags;
|
|
82
|
+
};
|