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:
Rohan Verma 2026-01-30 14:40:14 -08:00 committed by GitHub
commit bbe44818e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 36 additions and 57 deletions

View file

@ -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>;
} }

View file

@ -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