create persister

This commit is contained in:
Nate Kelley 2025-08-15 15:27:57 -06:00
parent 9b9b0f0bc5
commit 7e5344ccc6
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
8 changed files with 397 additions and 215 deletions

View File

@ -2,6 +2,7 @@
"name": "@buster-app/web-tss", "name": "@buster-app/web-tss",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "0.0.1",
"scripts": { "scripts": {
"dev": "vite dev --port 3000", "dev": "vite dev --port 3000",
"dev:fast": "pnpm run build && pnpm run start", "dev:fast": "pnpm run build && pnpm run start",
@ -61,14 +62,18 @@
"@tailwindcss/vite": "^4.1.12", "@tailwindcss/vite": "^4.1.12",
"@tanstack/db": "^0.1.3", "@tanstack/db": "^0.1.3",
"@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/query-async-storage-persister": "^5.85.3",
"@tanstack/query-db-collection": "0.1.1", "@tanstack/query-db-collection": "0.1.1",
"@tanstack/query-sync-storage-persister": "^5.85.3",
"@tanstack/react-db": "0.1.3", "@tanstack/react-db": "0.1.3",
"@tanstack/react-devtools": "^0.4.0", "@tanstack/react-devtools": "^0.4.0",
"@tanstack/react-form": "^1.19.2", "@tanstack/react-form": "^1.19.2",
"@tanstack/react-query": "^5.85.3", "@tanstack/react-query": "^5.85.3",
"@tanstack/react-query-devtools": "^5.85.3", "@tanstack/react-query-devtools": "^5.85.3",
"@tanstack/react-query-persist-client": "^5.85.3",
"@tanstack/react-router": "^1.131.14", "@tanstack/react-router": "^1.131.14",
"@tanstack/react-router-devtools": "^1.131.14", "@tanstack/react-router-devtools": "^1.131.14",
"@tanstack/react-router-ssr-query": "^1.131.14",
"@tanstack/react-router-with-query": "^1.130.17", "@tanstack/react-router-with-query": "^1.130.17",
"@tanstack/react-start": "^1.131.15", "@tanstack/react-start": "^1.131.15",
"@tanstack/react-store": "^0.7.3", "@tanstack/react-store": "^0.7.3",

View File

@ -78,42 +78,39 @@ const topItems: ISidebarList = {
const yourStuff: ISidebarGroup = { const yourStuff: ISidebarGroup = {
label: 'Your stuff', label: 'Your stuff',
id: 'your-stuff', id: 'your-stuff',
items: ( items: [
[ {
{ label: 'Metrics',
label: 'Metrics', icon: <ASSET_ICONS.metrics />,
icon: <ASSET_ICONS.metrics />, route: { to: '/app/metrics' },
route: { to: '/app/metrics' }, id: '/app/metrics',
id: '/app/metrics', preload: 'intent',
preload: 'intent', preloadDelay: 1000,
preloadDelay: 1000, },
}, {
{ label: 'Dashboards',
label: 'Dashboards', icon: <ASSET_ICONS.dashboards />,
icon: <ASSET_ICONS.dashboards />, route: { to: '/app/dashboards' },
route: { to: '/app/dashboards' }, id: '/app/dashboards/',
id: '/app/dashboards/', preload: 'intent',
preload: 'intent', preloadDelay: 1000,
preloadDelay: 1000, },
}, {
{ label: 'Collections',
label: 'Collections', icon: <ASSET_ICONS.collections />,
icon: <ASSET_ICONS.collections />, route: { to: '/app/collections' },
route: { to: '/app/collections' }, id: '/app/collections/',
id: '/app/collections/', preload: 'intent',
preload: 'intent', preloadDelay: 1000,
preloadDelay: 1000, },
}, {
{ label: 'Reports',
label: 'Reports', icon: <ASSET_ICONS.reports />,
icon: <ASSET_ICONS.reports />, route: { to: '/app/reports' },
route: { to: '/app/reports' }, id: '/app/reports/',
id: '/app/reports/', preload: 'intent',
preload: 'intent', },
show: process.env.NEXT_PUBLIC_ENABLE_REPORTS === 'true', ] satisfies (ISidebarItem & { show?: boolean })[],
},
] satisfies (ISidebarItem & { show?: boolean })[]
).filter((x) => x.show !== false),
}; };
const adminTools: ISidebarGroup = { const adminTools: ISidebarGroup = {
@ -155,7 +152,10 @@ const tryGroup = (showInvitePeople: boolean): ISidebarGroup => ({
id: 'leave-feedback', id: 'leave-feedback',
onClick: () => toggleContactSupportModal('feedback'), onClick: () => toggleContactSupportModal('feedback'),
}, },
].filter((x) => x.show !== false), ].reduce((acc, { show, ...item }) => {
if (show !== false) acc.push(item);
return acc;
}, [] as ISidebarItem[]),
}); });
export const SidebarPrimary = React.memo(() => { export const SidebarPrimary = React.memo(() => {

View File

@ -1,10 +1,13 @@
import type { QueryClient } from '@tanstack/react-query';
import type React from 'react'; import type React from 'react';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { BusterStyleProvider } from './BusterStyles'; import { BusterStyleProvider } from './BusterStyles';
import { QueryPersister } from './Query/QueryProvider';
import { import {
SupabaseContextProvider, SupabaseContextProvider,
type SupabaseContextType, type SupabaseContextType,
} from './Supabase/SupabaseContextProvider'; } from './Supabase/SupabaseContextProvider';
// import type { UseSupabaseUserContextType } from '@/lib/supabase'; // import type { UseSupabaseUserContextType } from '@/lib/supabase';
// import { BusterAssetsProvider } from './Assets/BusterAssetsProvider'; // import { BusterAssetsProvider } from './Assets/BusterAssetsProvider';
// import { AppLayoutProvider } from './BusterAppLayout'; // import { AppLayoutProvider } from './BusterAppLayout';
@ -21,15 +24,19 @@ import {
// clearLog: false // clears the console per group of renders (default: false) // clearLog: false // clears the console per group of renders (default: false)
// }); // });
export const RootProviders: React.FC<PropsWithChildren<SupabaseContextType>> = ({ type RootProvidersProps = PropsWithChildren<SupabaseContextType & { queryClient: QueryClient }>;
export const RootProviders: React.FC<RootProvidersProps> = ({
children, children,
user, user,
accessToken, accessToken,
queryClient,
}) => { }) => {
return ( return (
<SupabaseContextProvider user={user} accessToken={accessToken}> <QueryPersister queryClient={queryClient}>
<BusterStyleProvider>{children}</BusterStyleProvider> <SupabaseContextProvider user={user} accessToken={accessToken}>
{/* <BusterReactQueryProvider> <BusterStyleProvider>{children}</BusterStyleProvider>
{/* <BusterReactQueryProvider>
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>
<AppLayoutProvider> <AppLayoutProvider>
<BusterUserConfigProvider> <BusterUserConfigProvider>
@ -43,7 +50,8 @@ export const RootProviders: React.FC<PropsWithChildren<SupabaseContextType>> = (
</AppLayoutProvider> </AppLayoutProvider>
</HydrationBoundary> </HydrationBoundary>
</BusterReactQueryProvider> */} </BusterReactQueryProvider> */}
</SupabaseContextProvider> </SupabaseContextProvider>
</QueryPersister>
); );
}; };

View File

@ -0,0 +1,27 @@
import { isServer, type QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { PersistQueryClientProvider as TanstackPersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import React from 'react';
import { persistOptions } from '@/integrations/tanstack-query/create-persister';
import { userQueryKeys } from '../../api/query_keys/users';
export const QueryPersister = ({
children,
queryClient,
}: {
children: React.ReactNode;
queryClient: QueryClient;
}) => {
const [mounted, setMounted] = React.useState(false);
return (
<TanstackPersistQueryClientProvider
client={queryClient}
persistOptions={persistOptions}
onSuccess={() => {
setMounted(true);
}}
>
{mounted ? children : null}
</TanstackPersistQueryClientProvider>
);
};

View File

@ -0,0 +1,60 @@
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { hashKey, isServer } from '@tanstack/react-query';
import type { PersistQueryClientProviderProps } from '@tanstack/react-query-persist-client';
import { dictionariesQueryKeys } from '@/api/query_keys/dictionaries';
import { slackQueryKeys } from '@/api/query_keys/slack';
import packageJson from '../../../package.json';
const buster = packageJson.version;
export const PERSIST_TIME = 1000 * 60 * 60 * 24 * 3; // 3 days
const PERSISTED_QUERIES = [slackQueryKeys.slackGetChannels.queryKey].map(hashKey);
export const PERMANENT_QUERIES = [
dictionariesQueryKeys.getCurrencies.queryKey,
dictionariesQueryKeys.colorPalettes.queryKey,
].map(hashKey);
const ALL_PERSISTED_QUERIES = [...PERSISTED_QUERIES, ...PERMANENT_QUERIES];
const persisterAsync = createAsyncStoragePersister({
key: 'buster-query-cache',
storage: isServer ? undefined : window.localStorage,
throttleTime: 1500, // 1.5 seconds,
serialize: (client) => {
/*
* Make persisted queries appear stale on first load by setting the dataUpdatedAt to 1 (NOT 0)
* This way the query will be refetched from the server when it is first mounted AND we
* don't have to deal with the flash of stale data that would otherwise happen.
*/
for (const query of client.clientState.queries) {
const isPermanentQuery = PERMANENT_QUERIES.includes(query.queryHash);
if (!isPermanentQuery) {
console.log('setting dataUpdatedAt to 1', query.queryHash);
query.state.dataUpdatedAt = 1;
}
}
return JSON.stringify(client);
},
});
export const persistOptions: PersistQueryClientProviderProps['persistOptions'] = {
maxAge: PERSIST_TIME,
dehydrateOptions: {
shouldDehydrateQuery: (query) => {
const isList =
query.queryKey[1] === 'list' || query.queryKey[query.queryKey.length - 1] === 'list';
return isList || ALL_PERSISTED_QUERIES.includes(query.queryHash);
},
},
hydrateOptions: {
defaultOptions: {
queries: {
initialDataUpdatedAt: 0,
},
},
},
buster,
persister: persisterAsync,
};

View File

@ -1,6 +1,8 @@
import type { User } from '@supabase/supabase-js'; import type { User } from '@supabase/supabase-js';
import type { QueryClient } from '@tanstack/react-query'; import type { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createRouter as createTanstackRouter } from '@tanstack/react-router'; import { createRouter as createTanstackRouter } from '@tanstack/react-router';
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query';
import { routerWithQueryClient } from '@tanstack/react-router-with-query'; import { routerWithQueryClient } from '@tanstack/react-router-with-query';
import * as TanstackQuery from './integrations/tanstack-query/query-client'; import * as TanstackQuery from './integrations/tanstack-query/query-client';
import { routeTree } from './routeTree.gen'; import { routeTree } from './routeTree.gen';
@ -14,7 +16,7 @@ export interface AppRouterContext {
export const createRouter = () => { export const createRouter = () => {
const queryClient = TanstackQuery.getQueryClient(); const queryClient = TanstackQuery.getQueryClient();
return routerWithQueryClient( const router = routerWithQueryClient(
createTanstackRouter({ createTanstackRouter({
routeTree, routeTree,
context: { queryClient, user: null }, //context is defined in the root route context: { queryClient, user: null }, //context is defined in the root route
@ -30,6 +32,15 @@ export const createRouter = () => {
}), }),
queryClient queryClient
); );
// setupRouterSsrQueryIntegration({
// router,
// queryClient,
// // Disable auto-wrapping since we'll handle it ourselves
// wrapQueryClient: false,
// });
return router;
}; };
// Register the router instance for type safety // Register the router instance for type safety

View File

@ -10,16 +10,9 @@ import appCss from '../styles/styles.css?url';
export const Route = createRootRouteWithContext<AppRouterContext>()({ export const Route = createRootRouteWithContext<AppRouterContext>()({
head: () => ({ head: () => ({
meta: [ meta: [
{ { charSet: 'utf-8' },
charSet: 'utf-8', { name: 'viewport', content: 'width=device-width, initial-scale=1' },
}, { title: 'Buster' },
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'Buster',
},
], ],
links: [{ rel: 'stylesheet', href: appCss }], links: [{ rel: 'stylesheet', href: appCss }],
}), }),
@ -33,7 +26,7 @@ export const Route = createRootRouteWithContext<AppRouterContext>()({
}); });
function RootDocument({ children }: { children: React.ReactNode }) { function RootDocument({ children }: { children: React.ReactNode }) {
const { user, accessToken } = Route.useRouteContext(); const { user, accessToken, queryClient } = Route.useRouteContext();
return ( return (
<html lang="en"> <html lang="en">
@ -41,7 +34,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<HeadContent /> <HeadContent />
</head> </head>
<body> <body>
<RootProviders user={user} accessToken={accessToken}> <RootProviders user={user} accessToken={accessToken} queryClient={queryClient}>
{children} {children}
</RootProviders> </RootProviders>
<TanstackDevtools /> <TanstackDevtools />

File diff suppressed because it is too large Load Diff