123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
import { create } from "zustand";
|
|
import io, { Socket } from "socket.io-client";
|
|
import { SOCKET_BASE_URI } from "../constants/endpoints";
|
|
import { QueryClient } from "@tanstack/react-query";
|
|
import { toast } from "react-toastify";
|
|
import "react-toastify/dist/ReactToastify.css";
|
|
|
|
const queryClient = new QueryClient();
|
|
|
|
/**
|
|
* Global application state interface
|
|
*/
|
|
interface StoreState {
|
|
/** Active socket.io connections by namespace */
|
|
socketInstances: Record<string, Socket>;
|
|
/**
|
|
* Get or create socket connection for namespace
|
|
* @param namespace - Socket namespace (default: "/")
|
|
* @returns Socket instance
|
|
*/
|
|
getSocket: (namespace?: string) => Socket;
|
|
/**
|
|
* Disconnect and remove socket instance
|
|
* @param namespace - Socket namespace to disconnect
|
|
*/
|
|
disconnectSocket: (namespace: string) => void;
|
|
/** ComicVine scraping state */
|
|
comicvine: {
|
|
scrapingStatus: string;
|
|
};
|
|
/** Import job queue state and actions */
|
|
importJobQueue: {
|
|
successfulJobCount: number;
|
|
failedJobCount: number;
|
|
status: string | undefined;
|
|
mostRecentImport: string | null;
|
|
setStatus: (status: string) => void;
|
|
setJobCount: (jobType: string, count: number) => void;
|
|
setMostRecentImport: (fileName: string) => void;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Zustand store for global app state and socket management
|
|
*/
|
|
export const useStore = create<StoreState>((set, get) => ({
|
|
socketInstances: {},
|
|
|
|
getSocket: (namespace = "/") => {
|
|
const ns = namespace === "/" ? "" : namespace;
|
|
const existing = get().socketInstances[namespace];
|
|
// Return existing socket if it exists, regardless of connection state
|
|
// This prevents creating duplicate sockets during connection phase
|
|
if (existing) return existing;
|
|
|
|
const sessionId = localStorage.getItem("sessionId");
|
|
const socket = io(`${SOCKET_BASE_URI}${ns}`, {
|
|
transports: ["websocket"],
|
|
withCredentials: true,
|
|
query: { sessionId },
|
|
});
|
|
|
|
socket.on("sessionInitialized", (id) => localStorage.setItem("sessionId", id));
|
|
if (sessionId) socket.emit("call", "socket.resumeSession", { sessionId, namespace });
|
|
|
|
socket.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", ({ completedJobCount, failedJobCount, queueStatus }) =>
|
|
set((s) => ({ importJobQueue: { ...s.importJobQueue, successfulJobCount: completedJobCount, failedJobCount, status: queueStatus } }))
|
|
);
|
|
|
|
socket.on("LS_COVER_EXTRACTED", ({ completedJobCount, importResult }) =>
|
|
set((s) => ({ importJobQueue: { ...s.importJobQueue, successfulJobCount: completedJobCount, mostRecentImport: importResult.data.rawFileDetails.name } }))
|
|
);
|
|
|
|
socket.on("LS_COVER_EXTRACTION_FAILED", ({ failedJobCount }) =>
|
|
set((s) => ({ importJobQueue: { ...s.importJobQueue, failedJobCount } }))
|
|
);
|
|
|
|
socket.on("LS_IMPORT_QUEUE_DRAINED", () => {
|
|
set((s) => ({ importJobQueue: { ...s.importJobQueue, status: "drained" } }));
|
|
setTimeout(() => {
|
|
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
|
localStorage.removeItem("sessionId");
|
|
}, 500);
|
|
});
|
|
|
|
socket.on("CV_SCRAPING_STATUS", ({ message }) =>
|
|
set((s) => ({ comicvine: { ...s.comicvine, scrapingStatus: message } }))
|
|
);
|
|
|
|
socket.on("searchResultsAvailable", ({ query }) =>
|
|
toast(`Results found for query: ${JSON.stringify(query, null, 2)}`)
|
|
);
|
|
|
|
set((s) => ({ socketInstances: { ...s.socketInstances, [namespace]: socket } }));
|
|
return socket;
|
|
},
|
|
|
|
disconnectSocket: (namespace) => {
|
|
const socket = get().socketInstances[namespace];
|
|
if (socket) {
|
|
socket.disconnect();
|
|
set((s) => {
|
|
const { [namespace]: _, ...rest } = s.socketInstances;
|
|
return { socketInstances: rest };
|
|
});
|
|
}
|
|
},
|
|
|
|
comicvine: { scrapingStatus: "" },
|
|
|
|
importJobQueue: {
|
|
successfulJobCount: 0,
|
|
failedJobCount: 0,
|
|
status: undefined,
|
|
mostRecentImport: null,
|
|
setStatus: (status) => set((s) => ({ importJobQueue: { ...s.importJobQueue, status } })),
|
|
setJobCount: (jobType, count) => set((s) => ({
|
|
importJobQueue: { ...s.importJobQueue, [jobType === "successful" ? "successfulJobCount" : "failedJobCount"]: count }
|
|
})),
|
|
setMostRecentImport: (fileName) => set((s) => ({ importJobQueue: { ...s.importJobQueue, mostRecentImport: fileName } })),
|
|
},
|
|
}));
|