perf(dashboard): stop refetching on every tab switch; own the QueryClient
Two changes, together removing nearly all the redundant conversation/history calls visible in the network panel. Keep-alive modules: Messenger and Inbox were rendered from the tab ternary, so every tab change UNMOUNTED them — all their state died and returning re-hit the API. They now mount on first visit and stay mounted, hidden with CSS. Deliberately mount-on-first-visit rather than eager-mount-both, so opening the dashboard does not fire two modules' initial loads for a user who never opens either. QueryClient ownership: the SDK (0.2.0) takes React Query as a peer and falls back to its own client if it can't find one — but then its cache would be invisible to the CRM. Providing it here keeps SDK and CRM data in one store, so a write can invalidate a conversation list. Client is created in useState, not at module scope, so an SSR render can't leak one user's cache into another's. refetchOnWindowFocus is off: the socket already pushes live changes, so focus refetching was pure noise. Also declares onnxruntime-web@1.21.0, a PINNED peerDependency of @imgly/background-removal that npm prunes on any re-resolve. The committed lock had it, so `npm ci` worked and `npm install` silently broke the gallery build — declaring it directly makes it unprunable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+38
-10
@@ -12,8 +12,9 @@
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@insignia/iios-kernel-client": "^0.1.6",
|
||||
"@insignia/iios-messaging-ui": "^0.1.17",
|
||||
"@insignia/iios-messaging-ui": "^0.2.0",
|
||||
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tensorflow-models/coco-ssd": "^2.2.3",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@vladmandic/face-api": "^1.7.15",
|
||||
@@ -21,6 +22,7 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^1.21.0",
|
||||
"next": "16.2.9",
|
||||
"onnxruntime-web": "^1.21.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
@@ -1110,11 +1112,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@insignia/iios-messaging-ui": {
|
||||
"version": "0.1.17",
|
||||
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.17/iios-messaging-ui-0.1.17.tgz",
|
||||
"integrity": "sha512-pob/ldsvloFZ15BUyDyVdZb34MYQrH6aFAV27tleTgaJhnOeDf4HlJe19ftpW8S/h2TNhFMRhv6eOnYRLzEtxQ==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.2.0/iios-messaging-ui-0.2.0.tgz",
|
||||
"integrity": "sha512-eM3K9CMtPumTvpoL7FrX+RcdKylhIDgMDZ4/7GCGoe6P1lj+HcAMmUX38cyLx7qq21DaDMnUcq3RNMW69gsDNw==",
|
||||
"peerDependencies": {
|
||||
"@insignia/iios-kernel-client": "*",
|
||||
"@tanstack/react-query": ">=5",
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
@@ -1823,6 +1826,32 @@
|
||||
"tailwindcss": "4.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.101.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
|
||||
"integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.101.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
|
||||
"integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.101.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.8.tgz",
|
||||
@@ -2072,7 +2101,7 @@
|
||||
"version": "19.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
@@ -3454,7 +3483,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
@@ -6639,7 +6668,7 @@
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0.tgz",
|
||||
"integrity": "sha512-adzOe+7uI7lKz6pQNbAsLMQd2Fq5Jhmoxd8LZjJr8m3KvbFyiYyRxRiC57/XXD+jb18voppjeGAjoZmskXG+7A==",
|
||||
"peer": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flatbuffers": "^25.1.24",
|
||||
"guid-typescript": "^1.0.9",
|
||||
@@ -6653,14 +6682,13 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz",
|
||||
"integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/opencollective-postinstall": {
|
||||
"version": "2.0.3",
|
||||
|
||||
+3
-1
@@ -14,8 +14,9 @@
|
||||
"@huggingface/transformers": "^4.2.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@insignia/iios-kernel-client": "^0.1.6",
|
||||
"@insignia/iios-messaging-ui": "^0.1.17",
|
||||
"@insignia/iios-messaging-ui": "^0.2.0",
|
||||
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tensorflow-models/coco-ssd": "^2.2.3",
|
||||
"@tensorflow/tfjs": "^4.22.0",
|
||||
"@vladmandic/face-api": "^1.7.15",
|
||||
@@ -23,6 +24,7 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^1.21.0",
|
||||
"next": "16.2.9",
|
||||
"onnxruntime-web": "^1.21.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
|
||||
+9467
-1539
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import { Settings } from "./settings";
|
||||
import { Projects } from "./projects";
|
||||
import { NotificationCenter } from "./notification-center";
|
||||
import { RealtimeProvider } from "@/lib/realtime";
|
||||
import { CrmQueryProvider } from "@/lib/query-client";
|
||||
import { SmartGallery } from "./smart-gallery";
|
||||
import { Leads } from "./leads";
|
||||
import { Verify } from "./verify";
|
||||
@@ -32,6 +33,16 @@ export function Dashboard() {
|
||||
const [active, setActive] = useState("dashboard");
|
||||
// Deep link from global search: which conversation to focus once we switch tabs.
|
||||
const [deepLink, setDeepLink] = useState<{ surface: "messenger" | "inbox"; threadId: string } | null>(null);
|
||||
// Messenger and Inbox are KEPT MOUNTED once opened, and hidden with CSS when you switch tabs.
|
||||
// Rendering them from the ternary below unmounted them on every tab change, which threw away
|
||||
// their sockets' worth of state and re-hit the API on return. Mount-on-first-visit rather than
|
||||
// eager-mount-both, so the dashboard doesn't fire two modules' initial loads at boot.
|
||||
const [everOpened, setEverOpened] = useState<Set<string>>(new Set());
|
||||
useEffect(() => {
|
||||
if (active === "messenger" || active === "inbox") {
|
||||
setEverOpened((prev) => (prev.has(active) ? prev : new Set(prev).add(active)));
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
function navigateToConversation(surface: "messenger" | "inbox", threadId: string) {
|
||||
setActive(surface);
|
||||
@@ -72,6 +83,7 @@ export function Dashboard() {
|
||||
|
||||
return (
|
||||
<div className="dash-root" data-theme={theme}>
|
||||
<CrmQueryProvider>
|
||||
<RealtimeProvider>
|
||||
<Sidebar active={active} onSelect={setActive} />
|
||||
<div className="dash-main">
|
||||
@@ -79,12 +91,21 @@ export function Dashboard() {
|
||||
<div className="dash-content">
|
||||
<ToastProvider>
|
||||
<NotificationCenter active={active} onNavigate={navigateToConversation} />
|
||||
{active === "profile" ? <Profile />
|
||||
{everOpened.has("messenger") ? (
|
||||
<div hidden={active !== "messenger"} className="dash-keepalive">
|
||||
<MessengerSdk focusThreadId={deepLink?.surface === "messenger" ? deepLink.threadId : null} />
|
||||
</div>
|
||||
) : null}
|
||||
{everOpened.has("inbox") ? (
|
||||
<div hidden={active !== "inbox"} className="dash-keepalive">
|
||||
<InboxSdk focusThreadId={deepLink?.surface === "inbox" ? deepLink.threadId : null} />
|
||||
</div>
|
||||
) : null}
|
||||
{active === "messenger" || active === "inbox" ? null
|
||||
: active === "profile" ? <Profile />
|
||||
: active === "support" ? <Support />
|
||||
: active === "rules" ? <Rules />
|
||||
: active === "ai" ? <AiAssistant />
|
||||
: active === "messenger" ? <MessengerSdk focusThreadId={deepLink?.surface === "messenger" ? deepLink.threadId : null} />
|
||||
: active === "inbox" ? <InboxSdk focusThreadId={deepLink?.surface === "inbox" ? deepLink.threadId : null} />
|
||||
: active === "settings" ? <Settings />
|
||||
: active === "gallery" ? <SmartGallery theme={theme} />
|
||||
: active === "leads" ? <Leads />
|
||||
@@ -97,6 +118,7 @@ export function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
</RealtimeProvider>
|
||||
</CrmQueryProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,9 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
||||
|
||||
async history(threadId: string): Promise<Message[]> {
|
||||
if (this.socket) {
|
||||
const res = await this.socket.openThread(threadId); // joins so live events flow
|
||||
// openThread both JOINS the room and returns history, so it is the only call needed on a
|
||||
// cache miss. The SDK caches per thread, so this no longer fires on every selection.
|
||||
const res = await this.socket.openThread(threadId);
|
||||
this.joined.add(threadId);
|
||||
return Promise.all(
|
||||
res.history.map((m) => {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
// The CRM owns the single QueryClient for the whole dashboard.
|
||||
//
|
||||
// The messaging SDK takes React Query as a peerDependency and will fall back to its own client if
|
||||
// it can't find one — but then its cache would be invisible to us: we couldn't invalidate a
|
||||
// conversation list after a write, share GC policy, or persist it. Providing the client here keeps
|
||||
// SDK and CRM data in one store.
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
export function CrmQueryProvider({ children }: { children: ReactNode }) {
|
||||
// useState, not a module constant: a module-level client would be shared across SSR requests and
|
||||
// leak one user's cache into another's render.
|
||||
const [client] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// The socket already pushes live changes for anything the user has open, so refetching
|
||||
// on every window focus is noise — it was one of the redundant-call sources.
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
// Keep a cache entry ~10 min after its last observer unmounts, so leaving a module and
|
||||
// coming back is served from memory rather than the network.
|
||||
gcTime: 10 * 60_000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
|
||||
}
|
||||
Reference in New Issue
Block a user