mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +02:00
Merge pull request #755 from AnishSarkar22/fix/electric-sql
refactor: streamline initialization logic for electric-sql & electric logs will not show in production
This commit is contained in:
commit
bbe44818e7
2 changed files with 36 additions and 57 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
||||||
|
|
@ -18,15 +19,8 @@ interface ElectricProviderProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ElectricProvider initializes the Electric SQL client with user-specific PGlite database
|
* Initializes user-specific PGlite database with Electric SQL sync.
|
||||||
* and provides it to children via context.
|
* Handles user isolation, cleanup, and re-initialization on user change.
|
||||||
*
|
|
||||||
* KEY BEHAVIORS:
|
|
||||||
* 1. Single initialization point - only this provider creates the Electric client
|
|
||||||
* 2. Creates user-specific database (isolated per user)
|
|
||||||
* 3. Cleans up other users' databases on login
|
|
||||||
* 4. Re-initializes when user changes
|
|
||||||
* 5. Provides client via context - hooks should use useElectricClient()
|
|
||||||
*/
|
*/
|
||||||
export function ElectricProvider({ children }: ElectricProviderProps) {
|
export function ElectricProvider({ children }: ElectricProviderProps) {
|
||||||
const [electricClient, setElectricClient] = useState<ElectricClient | null>(null);
|
const [electricClient, setElectricClient] = useState<ElectricClient | null>(null);
|
||||||
|
|
@ -38,15 +32,13 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
|
||||||
} = useAtomValue(currentUserAtom);
|
} = useAtomValue(currentUserAtom);
|
||||||
const previousUserIdRef = useRef<string | null>(null);
|
const previousUserIdRef = useRef<string | null>(null);
|
||||||
const initializingRef = useRef(false);
|
const initializingRef = useRef(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip on server side
|
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
// If no user is logged in, don't initialize Electric
|
// No user logged in - cleanup if previous user existed
|
||||||
// The app can still function without real-time sync for non-authenticated pages
|
|
||||||
if (!isUserLoaded || !user?.id) {
|
if (!isUserLoaded || !user?.id) {
|
||||||
// If we had a previous user and now logged out, cleanup
|
|
||||||
if (previousUserIdRef.current && isElectricInitialized()) {
|
if (previousUserIdRef.current && isElectricInitialized()) {
|
||||||
console.log("[ElectricProvider] User logged out, cleaning up...");
|
console.log("[ElectricProvider] User logged out, cleaning up...");
|
||||||
cleanupElectric().then(() => {
|
cleanupElectric().then(() => {
|
||||||
|
|
@ -59,25 +51,17 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
|
||||||
|
|
||||||
const userId = String(user.id);
|
const userId = String(user.id);
|
||||||
|
|
||||||
// If already initialized for THIS user, skip
|
// Skip if already initialized for this user or currently initializing
|
||||||
if (electricClient && previousUserIdRef.current === userId) {
|
if ((electricClient && previousUserIdRef.current === userId) || initializingRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent concurrent initialization attempts
|
|
||||||
if (initializingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User changed or first initialization
|
|
||||||
initializingRef.current = true;
|
initializingRef.current = true;
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
console.log(`[ElectricProvider] Initializing for user: ${userId}`);
|
console.log(`[ElectricProvider] Initializing for user: ${userId}`);
|
||||||
|
|
||||||
// If different user was previously initialized, cleanup will happen inside initElectric
|
|
||||||
const client = await initElectric(userId);
|
const client = await initElectric(userId);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -90,7 +74,6 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
|
||||||
console.error("[ElectricProvider] Failed to initialize:", err);
|
console.error("[ElectricProvider] Failed to initialize:", err);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setError(err instanceof Error ? err : new Error("Failed to initialize Electric SQL"));
|
setError(err instanceof Error ? err : new Error("Failed to initialize Electric SQL"));
|
||||||
// Set client to null so hooks know initialization failed
|
|
||||||
setElectricClient(null);
|
setElectricClient(null);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -101,38 +84,33 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
};
|
};
|
||||||
}, [user?.id, isUserLoaded, electricClient]);
|
}, [user?.id, isUserLoaded, electricClient]);
|
||||||
|
|
||||||
// Check if user is authenticated first (has bearer token)
|
|
||||||
// This prevents showing loading screen for unauthenticated users on homepage
|
|
||||||
const hasToken = typeof window !== "undefined" && !!getBearerToken();
|
const hasToken = typeof window !== "undefined" && !!getBearerToken();
|
||||||
|
|
||||||
// Determine if we should show loading
|
// Only block UI on dashboard routes; public pages render immediately
|
||||||
const shouldShowLoading = hasToken && isUserLoaded && !!user?.id && !electricClient && !error;
|
const requiresElectricLoading = pathname?.startsWith("/dashboard");
|
||||||
|
const shouldShowLoading =
|
||||||
|
hasToken && isUserLoaded && !!user?.id && !electricClient && !error && requiresElectricLoading;
|
||||||
|
|
||||||
// Use global loading hook with ownership tracking - prevents flash during transitions
|
|
||||||
useGlobalLoadingEffect(shouldShowLoading);
|
useGlobalLoadingEffect(shouldShowLoading);
|
||||||
|
|
||||||
// For non-authenticated pages (like landing page), render immediately with null context
|
// Render immediately for unauthenticated users or failed user queries
|
||||||
// Also render immediately if user query failed (e.g., token expired)
|
|
||||||
if (!hasToken || !isUserLoaded || !user?.id || isUserError) {
|
if (!hasToken || !isUserLoaded || !user?.id || isUserError) {
|
||||||
return <ElectricContext.Provider value={null}>{children}</ElectricContext.Provider>;
|
return <ElectricContext.Provider value={null}>{children}</ElectricContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return children with null context while initializing - the global provider handles the loading UI
|
// Render with null context while initializing
|
||||||
if (!electricClient && !error) {
|
if (!electricClient && !error) {
|
||||||
return <ElectricContext.Provider value={null}>{children}</ElectricContext.Provider>;
|
return <ElectricContext.Provider value={null}>{children}</ElectricContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's an error, still render but warn
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.warn("[ElectricProvider] Initialization failed, sync may not work:", error.message);
|
console.warn("[ElectricProvider] Initialization failed, sync may not work:", error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide the Electric client to children
|
|
||||||
return <ElectricContext.Provider value={electricClient}>{children}</ElectricContext.Provider>;
|
return <ElectricContext.Provider value={electricClient}>{children}</ElectricContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,8 +165,8 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
||||||
dataDir: dbName,
|
dataDir: dbName,
|
||||||
relaxedDurability: true,
|
relaxedDurability: true,
|
||||||
extensions: {
|
extensions: {
|
||||||
// Enable debug mode in electricSync to see detailed sync logs
|
// Enable debug mode in electricSync only in development
|
||||||
electric: electricSync({ debug: true }),
|
electric: electricSync({ debug: process.env.NODE_ENV === "development" }),
|
||||||
live, // Enable live queries for real-time updates
|
live, // Enable live queries for real-time updates
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -341,26 +341,27 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
||||||
console.log("[Electric] Where clause:", where, "Validated:", validatedWhere);
|
console.log("[Electric] Where clause:", where, "Validated:", validatedWhere);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Debug: Test Electric SQL connection directly first
|
// Debug: Test Electric SQL connection directly first (DEV ONLY - skipped in production)
|
||||||
// Use validatedWhere to ensure proper URL encoding
|
if (process.env.NODE_ENV === "development") {
|
||||||
const testUrl = `${electricUrl}/v1/shape?table=${table}&offset=-1${validatedWhere ? `&where=${encodeURIComponent(validatedWhere)}` : ""}`;
|
const testUrl = `${electricUrl}/v1/shape?table=${table}&offset=-1${validatedWhere ? `&where=${encodeURIComponent(validatedWhere)}` : ""}`;
|
||||||
console.log("[Electric] Testing Electric SQL directly:", testUrl);
|
console.log("[Electric] Testing Electric SQL directly:", testUrl);
|
||||||
try {
|
try {
|
||||||
const testResponse = await fetch(testUrl);
|
const testResponse = await fetch(testUrl);
|
||||||
const testHeaders = {
|
const testHeaders = {
|
||||||
handle: testResponse.headers.get("electric-handle"),
|
handle: testResponse.headers.get("electric-handle"),
|
||||||
offset: testResponse.headers.get("electric-offset"),
|
offset: testResponse.headers.get("electric-offset"),
|
||||||
upToDate: testResponse.headers.get("electric-up-to-date"),
|
upToDate: testResponse.headers.get("electric-up-to-date"),
|
||||||
};
|
};
|
||||||
console.log("[Electric] Direct Electric SQL response headers:", testHeaders);
|
console.log("[Electric] Direct Electric SQL response headers:", testHeaders);
|
||||||
const testData = await testResponse.json();
|
const testData = await testResponse.json();
|
||||||
console.log(
|
console.log(
|
||||||
"[Electric] Direct Electric SQL data count:",
|
"[Electric] Direct Electric SQL data count:",
|
||||||
Array.isArray(testData) ? testData.length : "not array",
|
Array.isArray(testData) ? testData.length : "not array",
|
||||||
testData
|
testData
|
||||||
);
|
);
|
||||||
} catch (testErr) {
|
} catch (testErr) {
|
||||||
console.error("[Electric] Direct Electric SQL test failed:", testErr);
|
console.error("[Electric] Direct Electric SQL test failed:", testErr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use PGlite's electric sync plugin to sync the shape
|
// Use PGlite's electric sync plugin to sync the shape
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue