2026-03-07 02:34:23 +05:30
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
|
import { useElectricClient } from "@/lib/electric/context";
|
|
|
|
|
|
2026-03-08 21:16:52 +05:30
|
|
|
export type DocumentsProcessingStatus = "idle" | "processing" | "success" | "error";
|
|
|
|
|
|
|
|
|
|
const SUCCESS_LINGER_MS = 5000;
|
|
|
|
|
|
2026-03-07 02:34:23 +05:30
|
|
|
/**
|
2026-03-08 21:16:52 +05:30
|
|
|
* Returns the processing status of documents in the search space:
|
|
|
|
|
* - "processing" — at least one doc is pending/processing (show spinner)
|
|
|
|
|
* - "error" — nothing processing, but failed docs exist (show red icon)
|
|
|
|
|
* - "success" — just transitioned from processing → all clear (green check, auto-dismisses)
|
|
|
|
|
* - "idle" — nothing noteworthy (show normal icon)
|
2026-03-07 02:34:23 +05:30
|
|
|
*/
|
2026-03-08 21:16:52 +05:30
|
|
|
export function useDocumentsProcessing(searchSpaceId: number | null): DocumentsProcessingStatus {
|
2026-03-07 02:34:23 +05:30
|
|
|
const electricClient = useElectricClient();
|
2026-03-08 21:16:52 +05:30
|
|
|
const [status, setStatus] = useState<DocumentsProcessingStatus>("idle");
|
2026-03-07 02:34:23 +05:30
|
|
|
const liveQueryRef = useRef<{ unsubscribe?: () => void } | null>(null);
|
2026-03-08 21:16:52 +05:30
|
|
|
const wasProcessingRef = useRef(false);
|
|
|
|
|
const successTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
2026-03-07 02:34:23 +05:30
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!searchSpaceId || !electricClient) return;
|
|
|
|
|
|
|
|
|
|
const spaceId = searchSpaceId;
|
|
|
|
|
const client = electricClient;
|
|
|
|
|
let mounted = true;
|
|
|
|
|
|
|
|
|
|
async function setup() {
|
|
|
|
|
if (liveQueryRef.current) {
|
|
|
|
|
try {
|
|
|
|
|
liveQueryRef.current.unsubscribe?.();
|
|
|
|
|
} catch {
|
|
|
|
|
/* PGlite may be closed */
|
|
|
|
|
}
|
|
|
|
|
liveQueryRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const handle = await client.syncShape({
|
|
|
|
|
table: "documents",
|
|
|
|
|
where: `search_space_id = ${spaceId}`,
|
|
|
|
|
columns: [
|
|
|
|
|
"id",
|
|
|
|
|
"document_type",
|
|
|
|
|
"search_space_id",
|
|
|
|
|
"title",
|
|
|
|
|
"created_by_id",
|
|
|
|
|
"created_at",
|
|
|
|
|
"status",
|
|
|
|
|
],
|
|
|
|
|
primaryKey: ["id"],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
|
|
if (!handle.isUpToDate && handle.initialSyncPromise) {
|
|
|
|
|
await Promise.race([
|
|
|
|
|
handle.initialSyncPromise,
|
|
|
|
|
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
|
|
const db = client.db as {
|
|
|
|
|
live?: {
|
|
|
|
|
query: <T>(
|
|
|
|
|
sql: string,
|
2026-03-07 04:46:48 +05:30
|
|
|
params?: (number | string)[]
|
2026-03-07 02:34:23 +05:30
|
|
|
) => Promise<{
|
|
|
|
|
subscribe: (cb: (result: { rows: T[] }) => void) => void;
|
|
|
|
|
unsubscribe?: () => void;
|
|
|
|
|
}>;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!db.live?.query) return;
|
|
|
|
|
|
2026-03-08 21:16:52 +05:30
|
|
|
const liveQuery = await db.live.query<{
|
|
|
|
|
processing_count: number | string;
|
|
|
|
|
failed_count: number | string;
|
|
|
|
|
}>(
|
|
|
|
|
`SELECT
|
|
|
|
|
SUM(CASE WHEN status->>'state' IN ('pending', 'processing') THEN 1 ELSE 0 END) AS processing_count,
|
|
|
|
|
SUM(CASE WHEN status->>'state' = 'failed' THEN 1 ELSE 0 END) AS failed_count
|
|
|
|
|
FROM documents
|
|
|
|
|
WHERE search_space_id = $1`,
|
2026-03-07 04:46:48 +05:30
|
|
|
[spaceId]
|
2026-03-07 02:34:23 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!mounted) {
|
|
|
|
|
liveQuery.unsubscribe?.();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 21:16:52 +05:30
|
|
|
liveQuery.subscribe(
|
2026-03-08 21:17:45 +05:30
|
|
|
(result: {
|
|
|
|
|
rows: Array<{ processing_count: number | string; failed_count: number | string }>;
|
|
|
|
|
}) => {
|
2026-03-08 21:16:52 +05:30
|
|
|
if (!mounted || !result.rows?.[0]) return;
|
|
|
|
|
|
|
|
|
|
const processingCount = Number(result.rows[0].processing_count) || 0;
|
|
|
|
|
const failedCount = Number(result.rows[0].failed_count) || 0;
|
|
|
|
|
|
|
|
|
|
if (processingCount > 0) {
|
|
|
|
|
wasProcessingRef.current = true;
|
|
|
|
|
if (successTimerRef.current) {
|
|
|
|
|
clearTimeout(successTimerRef.current);
|
|
|
|
|
successTimerRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
setStatus("processing");
|
|
|
|
|
} else if (failedCount > 0) {
|
|
|
|
|
wasProcessingRef.current = false;
|
|
|
|
|
if (successTimerRef.current) {
|
|
|
|
|
clearTimeout(successTimerRef.current);
|
|
|
|
|
successTimerRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
setStatus("error");
|
|
|
|
|
} else if (wasProcessingRef.current) {
|
|
|
|
|
wasProcessingRef.current = false;
|
|
|
|
|
setStatus("success");
|
|
|
|
|
if (successTimerRef.current) {
|
|
|
|
|
clearTimeout(successTimerRef.current);
|
|
|
|
|
}
|
|
|
|
|
successTimerRef.current = setTimeout(() => {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
setStatus("idle");
|
|
|
|
|
successTimerRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
}, SUCCESS_LINGER_MS);
|
|
|
|
|
} else {
|
|
|
|
|
setStatus("idle");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-03-07 02:34:23 +05:30
|
|
|
|
|
|
|
|
liveQueryRef.current = liveQuery;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("[useDocumentsProcessing] Electric setup failed:", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setup();
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
mounted = false;
|
2026-03-08 21:16:52 +05:30
|
|
|
if (successTimerRef.current) {
|
|
|
|
|
clearTimeout(successTimerRef.current);
|
|
|
|
|
successTimerRef.current = null;
|
|
|
|
|
}
|
2026-03-07 02:34:23 +05:30
|
|
|
if (liveQueryRef.current) {
|
|
|
|
|
try {
|
|
|
|
|
liveQueryRef.current.unsubscribe?.();
|
|
|
|
|
} catch {
|
|
|
|
|
/* PGlite may be closed */
|
|
|
|
|
}
|
|
|
|
|
liveQueryRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [searchSpaceId, electricClient]);
|
|
|
|
|
|
2026-03-08 21:16:52 +05:30
|
|
|
return status;
|
2026-03-07 02:34:23 +05:30
|
|
|
}
|