mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-02 22:01:05 +02:00
feat: implement responsive comment UI with mobile and desktop variants
This commit is contained in:
parent
d83e9aa52d
commit
ab91cbd148
8 changed files with 229 additions and 9 deletions
|
|
@ -6,7 +6,7 @@ import {
|
||||||
useAssistantState,
|
useAssistantState,
|
||||||
} from "@assistant-ui/react";
|
} from "@assistant-ui/react";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { CheckIcon, CopyIcon, DownloadIcon, RefreshCwIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
|
@ -23,8 +23,11 @@ import {
|
||||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
||||||
|
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
||||||
import { CommentTrigger } from "@/components/chat-comments/comment-trigger/comment-trigger";
|
import { CommentTrigger } from "@/components/chat-comments/comment-trigger/comment-trigger";
|
||||||
import { useComments } from "@/hooks/use-comments";
|
import { useComments } from "@/hooks/use-comments";
|
||||||
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export const MessageError: FC = () => {
|
export const MessageError: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -93,6 +96,7 @@ function parseMessageId(assistantUiMessageId: string | undefined): number | null
|
||||||
|
|
||||||
export const AssistantMessage: FC = () => {
|
export const AssistantMessage: FC = () => {
|
||||||
const [messageHeight, setMessageHeight] = useState<number | undefined>(undefined);
|
const [messageHeight, setMessageHeight] = useState<number | undefined>(undefined);
|
||||||
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||||
const messageRef = useRef<HTMLDivElement>(null);
|
const messageRef = useRef<HTMLDivElement>(null);
|
||||||
const messageId = useAssistantState(({ message }) => message?.id);
|
const messageId = useAssistantState(({ message }) => message?.id);
|
||||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||||
|
|
@ -102,6 +106,11 @@ export const AssistantMessage: FC = () => {
|
||||||
addingCommentToMessageIdAtom
|
addingCommentToMessageIdAtom
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Screen size detection for responsive comment UI
|
||||||
|
// Mobile: < 768px (bottom sheet), Medium: 768px - 1024px (right sheet), Desktop: >= 1024px (inline panel)
|
||||||
|
const isMediumScreen = useMediaQuery("(min-width: 768px) and (max-width: 1023px)");
|
||||||
|
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||||
|
|
||||||
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
||||||
const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false);
|
const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false);
|
||||||
const isMessageStreaming = isThreadRunning && isLastMessage;
|
const isMessageStreaming = isThreadRunning && isLastMessage;
|
||||||
|
|
@ -111,7 +120,8 @@ export const AssistantMessage: FC = () => {
|
||||||
enabled: !!dbMessageId,
|
enabled: !!dbMessageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasComments = (commentsData?.total_count ?? 0) > 0;
|
const commentCount = commentsData?.total_count ?? 0;
|
||||||
|
const hasComments = commentCount > 0;
|
||||||
const isAddingComment = dbMessageId !== null && addingCommentToMessageId === dbMessageId;
|
const isAddingComment = dbMessageId !== null && addingCommentToMessageId === dbMessageId;
|
||||||
const showCommentPanel = hasComments || isAddingComment;
|
const showCommentPanel = hasComments || isAddingComment;
|
||||||
|
|
||||||
|
|
@ -120,6 +130,10 @@ export const AssistantMessage: FC = () => {
|
||||||
setAddingCommentToMessageId(isAddingComment ? null : dbMessageId);
|
setAddingCommentToMessageId(isAddingComment ? null : dbMessageId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCommentTriggerClick = () => {
|
||||||
|
setIsSheetOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!messageRef.current) return;
|
if (!messageRef.current) return;
|
||||||
const el = messageRef.current;
|
const el = messageRef.current;
|
||||||
|
|
@ -130,6 +144,11 @@ export const AssistantMessage: FC = () => {
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
|
||||||
|
|
||||||
|
// Determine sheet side based on screen size
|
||||||
|
const sheetSide = isMediumScreen ? "right" : "bottom";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessagePrimitive.Root
|
<MessagePrimitive.Root
|
||||||
ref={messageRef}
|
ref={messageRef}
|
||||||
|
|
@ -138,6 +157,7 @@ export const AssistantMessage: FC = () => {
|
||||||
>
|
>
|
||||||
<AssistantMessageInner />
|
<AssistantMessageInner />
|
||||||
|
|
||||||
|
{/* Desktop comment panel - only on lg screens and above */}
|
||||||
{searchSpaceId && commentsEnabled && !isMessageStreaming && (
|
{searchSpaceId && commentsEnabled && !isMessageStreaming && (
|
||||||
<div className="absolute left-full top-0 ml-4 hidden lg:block w-72">
|
<div className="absolute left-full top-0 ml-4 hidden lg:block w-72">
|
||||||
<div
|
<div
|
||||||
|
|
@ -168,6 +188,40 @@ export const AssistantMessage: FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Mobile & Medium screen comment trigger - shown below lg breakpoint */}
|
||||||
|
{showCommentTrigger && !isDesktop && (
|
||||||
|
<div className="mt-2 flex justify-start">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCommentTriggerClick}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 rounded-full px-3 py-1.5 text-sm transition-colors",
|
||||||
|
hasComments
|
||||||
|
? "border border-primary/50 bg-primary/5 text-primary hover:bg-primary/10"
|
||||||
|
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MessageSquare className={cn("size-4", hasComments && "fill-current")} />
|
||||||
|
{hasComments ? (
|
||||||
|
<span>{commentCount} {commentCount === 1 ? "comment" : "comments"}</span>
|
||||||
|
) : (
|
||||||
|
<span>Add comment</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Comment sheet - bottom for mobile, right for medium screens */}
|
||||||
|
{showCommentTrigger && !isDesktop && (
|
||||||
|
<CommentSheet
|
||||||
|
messageId={dbMessageId}
|
||||||
|
isOpen={isSheetOpen}
|
||||||
|
onOpenChange={setIsSheetOpen}
|
||||||
|
commentCount={commentCount}
|
||||||
|
side={sheetSide}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</MessagePrimitive.Root>
|
</MessagePrimitive.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export function CommentPanelContainer({
|
||||||
messageId,
|
messageId,
|
||||||
isOpen,
|
isOpen,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
|
variant = "desktop",
|
||||||
}: CommentPanelContainerProps) {
|
}: CommentPanelContainerProps) {
|
||||||
const { data: commentsData, isLoading: isCommentsLoading } = useComments({
|
const { data: commentsData, isLoading: isCommentsLoading } = useComments({
|
||||||
messageId,
|
messageId,
|
||||||
|
|
@ -80,6 +81,7 @@ export function CommentPanelContainer({
|
||||||
onDeleteComment={handleDeleteComment}
|
onDeleteComment={handleDeleteComment}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
|
variant={variant}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,6 @@ export interface CommentPanelContainerProps {
|
||||||
messageId: number;
|
messageId: number;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
|
/** Variant for responsive styling - desktop shows border/bg, mobile is plain */
|
||||||
|
variant?: "desktop" | "mobile";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { MessageSquarePlus } from "lucide-react";
|
import { MessageSquarePlus } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { CommentComposer } from "../comment-composer/comment-composer";
|
import { CommentComposer } from "../comment-composer/comment-composer";
|
||||||
import { CommentThread } from "../comment-thread/comment-thread";
|
import { CommentThread } from "../comment-thread/comment-thread";
|
||||||
import type { CommentPanelProps } from "./types";
|
import type { CommentPanelProps } from "./types";
|
||||||
|
|
@ -18,6 +19,7 @@ export function CommentPanel({
|
||||||
onDeleteComment,
|
onDeleteComment,
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
|
variant = "desktop",
|
||||||
}: CommentPanelProps) {
|
}: CommentPanelProps) {
|
||||||
const [isComposerOpen, setIsComposerOpen] = useState(false);
|
const [isComposerOpen, setIsComposerOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -30,9 +32,14 @@ export function CommentPanel({
|
||||||
setIsComposerOpen(false);
|
setIsComposerOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMobile = variant === "mobile";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[120px] w-96 items-center justify-center rounded-lg border bg-card p-4">
|
<div className={cn(
|
||||||
|
"flex min-h-[120px] items-center justify-center p-4",
|
||||||
|
!isMobile && "w-96 rounded-lg border bg-card"
|
||||||
|
)}>
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<div className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
<div className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
Loading comments...
|
Loading comments...
|
||||||
|
|
@ -50,8 +57,11 @@ export function CommentPanel({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex w-85 flex-col rounded-lg border bg-card"
|
className={cn(
|
||||||
style={effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined}
|
"flex flex-col",
|
||||||
|
isMobile ? "w-full" : "w-85 rounded-lg border bg-card"
|
||||||
|
)}
|
||||||
|
style={!isMobile && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined}
|
||||||
>
|
>
|
||||||
{hasThreads && (
|
{hasThreads && (
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin">
|
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin">
|
||||||
|
|
@ -82,7 +92,11 @@ export function CommentPanel({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={showEmptyState ? "border-t p-3" : "p-3"}>
|
<div className={cn(
|
||||||
|
"p-3",
|
||||||
|
showEmptyState && !isMobile && "border-t",
|
||||||
|
isMobile && "border-t"
|
||||||
|
)}>
|
||||||
{isComposerOpen ? (
|
{isComposerOpen ? (
|
||||||
<CommentComposer
|
<CommentComposer
|
||||||
members={members}
|
members={members}
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,6 @@ export interface CommentPanelProps {
|
||||||
onDeleteComment: (commentId: number) => void;
|
onDeleteComment: (commentId: number) => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
|
/** Variant for responsive styling - desktop shows border/bg, mobile is plain */
|
||||||
|
variant?: "desktop" | "mobile";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { MessageSquare } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CommentPanelContainer } from "../comment-panel-container/comment-panel-container";
|
||||||
|
import type { CommentSheetProps } from "./types";
|
||||||
|
|
||||||
|
export function CommentSheet({
|
||||||
|
messageId,
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
commentCount = 0,
|
||||||
|
side = "bottom",
|
||||||
|
}: CommentSheetProps) {
|
||||||
|
const isBottomSheet = side === "bottom";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent
|
||||||
|
side={side}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col p-0",
|
||||||
|
isBottomSheet
|
||||||
|
? "h-[85vh] max-h-[85vh] rounded-t-xl"
|
||||||
|
: "h-full w-full max-w-md"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Drag handle indicator - only for bottom sheet */}
|
||||||
|
{isBottomSheet && (
|
||||||
|
<div className="flex justify-center pt-3 pb-1">
|
||||||
|
<div className="h-1 w-10 rounded-full bg-muted-foreground/30" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SheetHeader className={cn(
|
||||||
|
"flex-shrink-0 border-b px-4",
|
||||||
|
isBottomSheet ? "pb-3" : "py-4"
|
||||||
|
)}>
|
||||||
|
<SheetTitle className="flex items-center gap-2 text-base font-semibold">
|
||||||
|
<MessageSquare className="size-5" />
|
||||||
|
Comments
|
||||||
|
{commentCount > 0 && (
|
||||||
|
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||||
|
{commentCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||||
|
<CommentPanelContainer
|
||||||
|
messageId={messageId}
|
||||||
|
isOpen={true}
|
||||||
|
variant="mobile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface CommentSheetProps {
|
||||||
|
messageId: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
commentCount?: number;
|
||||||
|
/** Side to open the sheet from - bottom for mobile, right for medium screens */
|
||||||
|
side?: "bottom" | "right";
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,10 @@ let initPromise: Promise<ElectricClient> | null = null;
|
||||||
// Cache for sync handles to prevent duplicate subscriptions (memory optimization)
|
// Cache for sync handles to prevent duplicate subscriptions (memory optimization)
|
||||||
const activeSyncHandles = new Map<string, SyncHandle>();
|
const activeSyncHandles = new Map<string, SyncHandle>();
|
||||||
|
|
||||||
|
// Track pending sync operations to prevent race conditions
|
||||||
|
// If a sync is in progress, subsequent calls will wait for it instead of starting a new one
|
||||||
|
const pendingSyncs = new Map<string, Promise<SyncHandle>>();
|
||||||
|
|
||||||
// Version for sync state - increment this to force fresh sync when Electric config changes
|
// Version for sync state - increment this to force fresh sync when Electric config changes
|
||||||
// Set to v2 for user-specific database architecture
|
// Set to v2 for user-specific database architecture
|
||||||
const SYNC_VERSION = 2;
|
const SYNC_VERSION = 2;
|
||||||
|
|
@ -256,7 +260,16 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
||||||
return existingHandle;
|
return existingHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build params for the shape request
|
// Check if there's already a pending sync for this shape (prevent race condition)
|
||||||
|
const pendingSync = pendingSyncs.get(cacheKey);
|
||||||
|
if (pendingSync) {
|
||||||
|
console.log(`[Electric] Waiting for pending sync to complete: ${cacheKey}`);
|
||||||
|
return pendingSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and track the sync promise to prevent race conditions
|
||||||
|
const syncPromise = (async (): Promise<SyncHandle> => {
|
||||||
|
// Build params for the shape request
|
||||||
// Electric SQL expects params as URL query parameters
|
// Electric SQL expects params as URL query parameters
|
||||||
const params: Record<string, string> = { table };
|
const params: Record<string, string> = { table };
|
||||||
|
|
||||||
|
|
@ -407,7 +420,55 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
||||||
) => Promise<{ unsubscribe: () => void; isUpToDate: boolean; stream: unknown }>;
|
) => Promise<{ unsubscribe: () => void; isUpToDate: boolean; stream: unknown }>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const shape = await pgWithElectric.electric.syncShapeToTable(shapeConfig);
|
|
||||||
|
let shape: { unsubscribe: () => void; isUpToDate: boolean; stream: unknown };
|
||||||
|
try {
|
||||||
|
shape = await pgWithElectric.electric.syncShapeToTable(shapeConfig);
|
||||||
|
} catch (syncError) {
|
||||||
|
// Handle "Already syncing" error - pglite-sync might not have fully cleaned up yet
|
||||||
|
const errorMessage = syncError instanceof Error ? syncError.message : String(syncError);
|
||||||
|
if (errorMessage.includes("Already syncing")) {
|
||||||
|
console.warn(`[Electric] Already syncing ${table}, waiting for existing sync to settle...`);
|
||||||
|
|
||||||
|
// Wait a short time for pglite-sync to settle
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Check if an active handle now exists (another sync might have completed)
|
||||||
|
const existingHandle = activeSyncHandles.get(cacheKey);
|
||||||
|
if (existingHandle) {
|
||||||
|
console.log(`[Electric] Found existing handle after waiting: ${cacheKey}`);
|
||||||
|
return existingHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry once after waiting
|
||||||
|
console.log(`[Electric] Retrying sync for ${table}...`);
|
||||||
|
try {
|
||||||
|
shape = await pgWithElectric.electric.syncShapeToTable(shapeConfig);
|
||||||
|
} catch (retryError) {
|
||||||
|
const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
|
||||||
|
if (retryMessage.includes("Already syncing")) {
|
||||||
|
// Still syncing - create a placeholder handle that indicates the table is being synced
|
||||||
|
console.warn(`[Electric] ${table} still syncing, creating placeholder handle`);
|
||||||
|
const placeholderHandle: SyncHandle = {
|
||||||
|
unsubscribe: () => {
|
||||||
|
console.log(`[Electric] Placeholder unsubscribe for: ${cacheKey}`);
|
||||||
|
activeSyncHandles.delete(cacheKey);
|
||||||
|
},
|
||||||
|
get isUpToDate() {
|
||||||
|
return false; // We don't know the real state
|
||||||
|
},
|
||||||
|
stream: undefined,
|
||||||
|
initialSyncPromise: Promise.resolve(), // Already syncing means data should be coming
|
||||||
|
};
|
||||||
|
activeSyncHandles.set(cacheKey, placeholderHandle);
|
||||||
|
return placeholderHandle;
|
||||||
|
}
|
||||||
|
throw retryError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw syncError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!shape) {
|
if (!shape) {
|
||||||
throw new Error("syncShapeToTable returned undefined");
|
throw new Error("syncShapeToTable returned undefined");
|
||||||
|
|
@ -568,6 +629,18 @@ export async function initElectric(userId: string): Promise<ElectricClient> {
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Track the sync promise to prevent concurrent syncs for the same shape
|
||||||
|
pendingSyncs.set(cacheKey, syncPromise);
|
||||||
|
|
||||||
|
// Clean up the pending sync when done (whether success or failure)
|
||||||
|
syncPromise.finally(() => {
|
||||||
|
pendingSyncs.delete(cacheKey);
|
||||||
|
console.log(`[Electric] Pending sync removed for: ${cacheKey}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return syncPromise;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -613,8 +686,9 @@ export async function cleanupElectric(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ensure cache is empty
|
// Ensure caches are empty
|
||||||
activeSyncHandles.clear();
|
activeSyncHandles.clear();
|
||||||
|
pendingSyncs.clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Close the PGlite database connection
|
// Close the PGlite database connection
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue