mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
Remove normalised cache (#2390)
* yeetus * fix * a * fix * todo * fix library stuff --------- Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com>
This commit is contained in:
parent
c76320e19d
commit
ce5e285c2f
1
.npmrc
1
.npmrc
|
@ -8,3 +8,4 @@ node-linker=hoisted
|
|||
auto-install-peers=true
|
||||
max-old-space-size=4096
|
||||
enable-pre-post-scripts=true
|
||||
package-manager-strict=false
|
||||
|
|
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -8147,15 +8147,6 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sd-cache"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sd-cli"
|
||||
version = "0.1.0"
|
||||
|
@ -8235,7 +8226,6 @@ dependencies = [
|
|||
"rspc",
|
||||
"sd-actors",
|
||||
"sd-ai",
|
||||
"sd-cache",
|
||||
"sd-cloud-api",
|
||||
"sd-core-file-path-helper",
|
||||
"sd-core-heavy-lifting",
|
||||
|
@ -8364,7 +8354,6 @@ name = "sd-core-prisma-helpers"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prisma-client-rust",
|
||||
"sd-cache",
|
||||
"sd-prisma",
|
||||
"serde",
|
||||
]
|
||||
|
@ -8672,7 +8661,6 @@ dependencies = [
|
|||
"prisma-client-rust",
|
||||
"rmp-serde",
|
||||
"rmpv",
|
||||
"sd-cache",
|
||||
"sd-sync",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -3,7 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|||
import { listen } from '@tauri-apps/api/event';
|
||||
import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { CacheProvider, createCache, RspcProvider } from '@sd/client';
|
||||
import { RspcProvider } from '@sd/client';
|
||||
import {
|
||||
createRoutes,
|
||||
ErrorPage,
|
||||
|
@ -56,16 +56,14 @@ export default function App() {
|
|||
return (
|
||||
<RspcProvider queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CacheProvider cache={cache}>
|
||||
{startupError ? (
|
||||
<ErrorPage
|
||||
message={startupError}
|
||||
submessage="Error occurred starting up the Spacedrive core"
|
||||
/>
|
||||
) : (
|
||||
<AppInner />
|
||||
)}
|
||||
</CacheProvider>
|
||||
{startupError ? (
|
||||
<ErrorPage
|
||||
message={startupError}
|
||||
submessage="Error occurred starting up the Spacedrive core"
|
||||
/>
|
||||
) : (
|
||||
<AppInner />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</RspcProvider>
|
||||
);
|
||||
|
@ -74,9 +72,7 @@ export default function App() {
|
|||
// we have a minimum delay between creating new tabs as react router can't handle creating tabs super fast
|
||||
const TAB_CREATE_DELAY = 150;
|
||||
|
||||
const cache = createCache();
|
||||
|
||||
const routes = createRoutes(platform, cache);
|
||||
const routes = createRoutes(platform);
|
||||
|
||||
type redirect = { pathname: string; search: string | undefined };
|
||||
|
||||
|
|
|
@ -18,9 +18,7 @@ import { MenuProvider } from 'react-native-popup-menu';
|
|||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import {
|
||||
CacheProvider,
|
||||
ClientContextProvider,
|
||||
createCache,
|
||||
initPlausible,
|
||||
LibraryContextProvider,
|
||||
P2PContextProvider,
|
||||
|
@ -146,7 +144,6 @@ function AppContainer() {
|
|||
}
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const cache = createCache();
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
|
@ -155,9 +152,7 @@ export default function App() {
|
|||
|
||||
return (
|
||||
<RspcProvider queryClient={queryClient}>
|
||||
<CacheProvider cache={cache}>
|
||||
<AppContainer />
|
||||
</CacheProvider>
|
||||
<AppContainer />
|
||||
</RspcProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { DotsThreeOutline, Plus } from 'phosphor-react-native';
|
||||
import { useRef } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
@ -22,8 +22,7 @@ const BrowseLocations = () => {
|
|||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const result = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const locations = useCache(result.data?.items);
|
||||
const locations = result.data;
|
||||
|
||||
return (
|
||||
<View style={tw`gap-5 px-6`}>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { DotsThreeOutline, Plus } from 'phosphor-react-native';
|
||||
import React, { useRef } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
@ -16,9 +16,7 @@ const BrowseTags = () => {
|
|||
const navigation = useNavigation<BrowseStackScreenProps<'Browse'>['navigation']>();
|
||||
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
|
||||
useNodes(tags.data?.nodes);
|
||||
const tagData = useCache(tags.data?.items);
|
||||
const tagData = tags.data;
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
useBridgeMutation,
|
||||
useCachedLibraries,
|
||||
useMultiZodForm,
|
||||
useNormalisedCache,
|
||||
useOnboardingStore,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
|
@ -69,12 +68,8 @@ const useFormState = () => {
|
|||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const cache = useNormalisedCache();
|
||||
const createLibrary = useBridgeMutation('library.create', {
|
||||
onSuccess: (libRaw) => {
|
||||
cache.withNodes(libRaw.nodes);
|
||||
const lib = cache.withCache(libRaw.item);
|
||||
|
||||
onSuccess: (lib) => {
|
||||
// We do this instead of invalidating the query because it triggers a full app re-render??
|
||||
insertLibrary(queryClient, lib);
|
||||
}
|
||||
|
@ -90,15 +85,13 @@ const useFormState = () => {
|
|||
|
||||
try {
|
||||
// show creation screen for a bit for smoothness
|
||||
const [libraryRaw] = await Promise.all([
|
||||
const [library] = await Promise.all([
|
||||
createLibrary.mutateAsync({
|
||||
name: data.NewLibrary.name,
|
||||
default_locations: null
|
||||
}),
|
||||
new Promise((res) => setTimeout(res, 500))
|
||||
]);
|
||||
cache.withNodes(libraryRaw.nodes);
|
||||
const library = cache.withCache(libraryRaw.item);
|
||||
|
||||
if (telemetryState.shareFullTelemetry) {
|
||||
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
|
||||
|
|
|
@ -2,15 +2,7 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import {
|
||||
arraysEqual,
|
||||
byteSize,
|
||||
Location,
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import { arraysEqual, byteSize, Location, useLibraryQuery, useOnlineLocations } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
|
@ -67,8 +59,7 @@ const DrawerLocations = () => {
|
|||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const result = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const locations = useCache(result.data?.items);
|
||||
const locations = result.data || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useRef } from 'react';
|
||||
import { ColorValue, Pressable, Text, View } from 'react-native';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
|
@ -38,8 +38,7 @@ const DrawerTags = () => {
|
|||
const tags = useLibraryQuery(['tags.list']);
|
||||
const navigation = useNavigation<DrawerNavigationHelpers>();
|
||||
|
||||
useNodes(tags.data?.nodes);
|
||||
const tagData = useCache(tags.data?.items);
|
||||
const tagData = tags.data || [];
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
|
|
|
@ -6,9 +6,7 @@ import {
|
|||
getItemFilePath,
|
||||
getItemObject,
|
||||
isPath,
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import { InfoPill, PlaceholderPill } from '~/components/primitive/InfoPill';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
@ -25,8 +23,7 @@ const InfoTagPills = ({ data, style }: Props) => {
|
|||
const tagsQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
|
||||
enabled: objectData != null
|
||||
});
|
||||
useNodes(tagsQuery.data?.nodes);
|
||||
const items = useCache(tagsQuery.data?.items);
|
||||
const items = tagsQuery.data;
|
||||
|
||||
const isDir = data && isPath(data) ? data.item.is_dir : false;
|
||||
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import {
|
||||
insertLibrary,
|
||||
useBridgeMutation,
|
||||
useNormalisedCache,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
import { insertLibrary, useBridgeMutation, usePlausibleEvent } from '@sd/client';
|
||||
import { Modal, ModalRef } from '~/components/layout/Modal';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import { ModalInput } from '~/components/primitive/Input';
|
||||
|
@ -18,7 +13,6 @@ const CreateLibraryModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
|||
const modalRef = useForwardedRef(ref);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const cache = useNormalisedCache();
|
||||
const [libName, setLibName] = useState('');
|
||||
|
||||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
|
@ -26,10 +20,7 @@ const CreateLibraryModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
|||
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
|
||||
'library.create',
|
||||
{
|
||||
onSuccess: (libRaw) => {
|
||||
cache.withNodes(libRaw.nodes);
|
||||
const lib = cache.withCache(libRaw.item);
|
||||
|
||||
onSuccess: (lib) => {
|
||||
// Reset form
|
||||
setLibName('');
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native';
|
|||
import React, { useRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack';
|
||||
|
||||
|
@ -19,63 +19,60 @@ const Locations = () => {
|
|||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverviewSection title="Locations" count={locations?.length}>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<FlatList
|
||||
horizontal
|
||||
data={locations}
|
||||
contentContainerStyle={tw`px-7`}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={(location) => location.id.toString()}
|
||||
ItemSeparatorComponent={() => <View style={tw`w-2`} />}
|
||||
ListEmptyComponent={() => {
|
||||
return (
|
||||
<NewCard
|
||||
style={twStyle(locations?.length !== 0 ? 'ml-2' : 'ml-0')}
|
||||
icons={['HDD', 'Folder', 'Globe', 'SD']}
|
||||
text="Connect a local path, volume or network location to Spacedrive."
|
||||
button={() => (
|
||||
<Button
|
||||
style={tw`mt-2.5`}
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
modalRef.current?.present();
|
||||
}}
|
||||
>
|
||||
<Text style={tw`font-bold text-ink`}>
|
||||
Add Location
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
navigation.jumpTo('BrowseStack', {
|
||||
initial: false,
|
||||
screen: 'Location',
|
||||
params: { id: item.id }
|
||||
})
|
||||
}
|
||||
>
|
||||
<StatCard
|
||||
connectionType={null}
|
||||
totalSpace={item.size_in_bytes || [0]}
|
||||
name={item.name || ''}
|
||||
color="#0362FF"
|
||||
icon="Folder"
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
/>
|
||||
<FlatList
|
||||
horizontal
|
||||
data={locations}
|
||||
contentContainerStyle={tw`px-7`}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={(location) => location.id.toString()}
|
||||
ItemSeparatorComponent={() => <View style={tw`w-2`} />}
|
||||
ListEmptyComponent={() => {
|
||||
return (
|
||||
<NewCard
|
||||
style={twStyle(locations?.length !== 0 ? 'ml-2' : 'ml-0')}
|
||||
icons={['HDD', 'Folder', 'Globe', 'SD']}
|
||||
text="Connect a local path, volume or network location to Spacedrive."
|
||||
button={() => (
|
||||
<Button
|
||||
style={tw`mt-2.5`}
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
modalRef.current?.present();
|
||||
}}
|
||||
>
|
||||
<Text style={tw`font-bold text-ink`}>Add Location</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
navigation.jumpTo('BrowseStack', {
|
||||
initial: false,
|
||||
screen: 'Location',
|
||||
params: { id: item.id }
|
||||
})
|
||||
}
|
||||
>
|
||||
<StatCard
|
||||
connectionType={null}
|
||||
totalSpace={item.size_in_bytes || [0]}
|
||||
name={item.name || ''}
|
||||
color="#0362FF"
|
||||
icon="Folder"
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</OverviewSection>
|
||||
<ImportModal ref={modalRef} />
|
||||
|
|
|
@ -2,7 +2,7 @@ import { MotiView } from 'moti';
|
|||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FlatList, Pressable, Text, View } from 'react-native';
|
||||
import { LinearTransition } from 'react-native-reanimated';
|
||||
import { Location, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Location, useLibraryQuery } from '@sd/client';
|
||||
import { Icon } from '~/components/icons/Icon';
|
||||
import Card from '~/components/layout/Card';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
|
@ -14,8 +14,7 @@ import { useSearchStore } from '~/stores/searchStore';
|
|||
|
||||
const Locations = () => {
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
const searchStore = useSearchStore();
|
||||
|
||||
return (
|
||||
|
|
|
@ -3,7 +3,7 @@ import { memo, useCallback, useMemo } from 'react';
|
|||
import { Pressable, Text, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { LinearTransition } from 'react-native-reanimated';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import Card from '~/components/layout/Card';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import Fade from '~/components/layout/Fade';
|
||||
|
@ -14,8 +14,7 @@ import { useSearchStore } from '~/stores/searchStore';
|
|||
|
||||
const Tags = () => {
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
useNodes(tags.data?.nodes);
|
||||
const tagsData = useCache(tags.data?.items);
|
||||
const tagsData = tags.data;
|
||||
const searchStore = useSearchStore();
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useCache, useLibraryQuery, useNodes, usePathsExplorerQuery } from '@sd/client';
|
||||
import { useLibraryQuery, usePathsExplorerQuery } from '@sd/client';
|
||||
import Explorer from '~/components/explorer/Explorer';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
import { getExplorerStore } from '~/stores/explorerStore';
|
||||
|
@ -8,8 +8,7 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
|
|||
const { id, path } = route.params;
|
||||
|
||||
const location = useLibraryQuery(['locations.get', route.params.id]);
|
||||
useNodes(location.data?.nodes);
|
||||
const locationData = useCache(location.data?.item);
|
||||
const locationData = location.data;
|
||||
|
||||
const paths = usePathsExplorerQuery({
|
||||
arg: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Plus } from 'phosphor-react-native';
|
|||
import { useMemo, useRef } from 'react';
|
||||
import { FlatList, Pressable, View } from 'react-native';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
|
@ -20,8 +20,7 @@ interface Props {
|
|||
|
||||
export default function LocationsScreen({ viewStyle }: Props) {
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
const { search } = useSearchStore();
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
const [debouncedSearch] = useDebounce(search, 200);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useCache, useLibraryQuery, useNodes, useObjectsExplorerQuery } from '@sd/client';
|
||||
import { useLibraryQuery, useObjectsExplorerQuery } from '@sd/client';
|
||||
import Explorer from '~/components/explorer/Explorer';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
||||
|
@ -7,8 +7,7 @@ export default function TagScreen({ navigation, route }: BrowseStackScreenProps<
|
|||
const { id } = route.params;
|
||||
|
||||
const tag = useLibraryQuery(['tags.get', id]);
|
||||
useNodes(tag.data?.nodes);
|
||||
const tagData = useCache(tag.data?.item);
|
||||
const tagData = tag.data;
|
||||
|
||||
const objects = useObjectsExplorerQuery({
|
||||
arg: { filters: [{ object: { tags: { in: [id] } } }], take: 30 },
|
||||
|
|
|
@ -3,7 +3,8 @@ import { Plus } from 'phosphor-react-native';
|
|||
import { useMemo, useRef } from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import Empty from '~/components/layout/Empty';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
|
@ -11,7 +12,6 @@ import CreateTagModal from '~/components/modal/tag/CreateTagModal';
|
|||
import { TagItem } from '~/components/tags/TagItem';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useSearchStore } from '~/stores/searchStore';
|
||||
|
||||
interface Props {
|
||||
|
@ -22,10 +22,9 @@ export default function TagsScreen({ viewStyle = 'list' }: Props) {
|
|||
const navigation = useNavigation<BrowseStackScreenProps<'Browse'>['navigation']>();
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const {search} = useSearchStore();
|
||||
const { search } = useSearchStore();
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
useNodes(tags.data?.nodes);
|
||||
const tagData = useCache(tags.data?.items);
|
||||
const tagData = tags.data || [];
|
||||
const [debouncedSearch] = useDebounce(search, 200);
|
||||
|
||||
const filteredTags = useMemo(
|
||||
|
@ -47,39 +46,39 @@ export default function TagsScreen({ viewStyle = 'list' }: Props) {
|
|||
>
|
||||
<Plus size={20} weight="bold" style={tw`text-ink`} />
|
||||
</Pressable>
|
||||
<FlatList
|
||||
data={filteredTags}
|
||||
renderItem={({ item }) => (
|
||||
<TagItem
|
||||
viewStyle={viewStyle}
|
||||
tag={item}
|
||||
onPress={() => {
|
||||
navigation.navigate('BrowseStack', {
|
||||
screen: 'Tag',
|
||||
params: { id: item.id, color: item.color! }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
<Empty
|
||||
icon="Tags"
|
||||
style={'border-0'}
|
||||
textSize="text-md"
|
||||
iconSize={84}
|
||||
description="You have not created any tags"
|
||||
/>
|
||||
}
|
||||
horizontal={false}
|
||||
numColumns={viewStyle === 'grid' ? 3 : 1}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
||||
contentContainerStyle={twStyle(
|
||||
`py-6`,
|
||||
tagData.length === 0 && 'h-full items-center justify-center'
|
||||
)}
|
||||
/>
|
||||
<FlatList
|
||||
data={filteredTags}
|
||||
renderItem={({ item }) => (
|
||||
<TagItem
|
||||
viewStyle={viewStyle}
|
||||
tag={item}
|
||||
onPress={() => {
|
||||
navigation.navigate('BrowseStack', {
|
||||
screen: 'Tag',
|
||||
params: { id: item.id, color: item.color! }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
<Empty
|
||||
icon="Tags"
|
||||
style={'border-0'}
|
||||
textSize="text-md"
|
||||
iconSize={84}
|
||||
description="You have not created any tags"
|
||||
/>
|
||||
}
|
||||
horizontal={false}
|
||||
numColumns={viewStyle === 'grid' ? 3 : 1}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
||||
contentContainerStyle={twStyle(
|
||||
`py-6`,
|
||||
tagData.length === 0 && 'h-full items-center justify-center'
|
||||
)}
|
||||
/>
|
||||
<CreateTagModal ref={modalRef} />
|
||||
</ScreenContainer>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { DotsThreeOutlineVertical, Pen, Trash } from 'phosphor-react-native';
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { Animated, FlatList, Pressable, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { LibraryConfigWrapped, useBridgeQuery, useCache, useNodes } from '@sd/client';
|
||||
import { LibraryConfigWrapped, useBridgeQuery } from '@sd/client';
|
||||
import Fade from '~/components/layout/Fade';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
|
@ -80,8 +80,7 @@ function LibraryItem({
|
|||
|
||||
const LibrarySettingsScreen = ({ navigation }: SettingsStackScreenProps<'LibrarySettings'>) => {
|
||||
const libraryList = useBridgeQuery(['library.list']);
|
||||
useNodes(libraryList.data?.nodes);
|
||||
const libraries = useCache(libraryList.data?.items);
|
||||
const libraries = libraryList.data;
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useEffect } from 'react';
|
|||
import { Controller } from 'react-hook-form';
|
||||
import { Alert, Text, View } from 'react-native';
|
||||
import { z } from 'zod';
|
||||
import { useLibraryMutation, useLibraryQuery, useNormalisedCache, useZodForm } from '@sd/client';
|
||||
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { AnimatedButton } from '~/components/primitive/Button';
|
||||
import { Divider } from '~/components/primitive/Divider';
|
||||
|
@ -33,7 +33,6 @@ const EditLocationSettingsScreen = ({
|
|||
const { id } = route.params;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
const form = useZodForm({ schema });
|
||||
|
||||
|
@ -92,10 +91,7 @@ const EditLocationSettingsScreen = ({
|
|||
}, [form, navigation, onSubmit]);
|
||||
|
||||
useLibraryQuery(['locations.getWithRules', id], {
|
||||
onSuccess: (dataRaw) => {
|
||||
cache.withNodes(dataRaw?.nodes);
|
||||
const data = cache.withCache(dataRaw?.item);
|
||||
|
||||
onSuccess: (data) => {
|
||||
if (data && !form.formState.isDirty)
|
||||
form.reset({
|
||||
displayName: data.name,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { CacheProvider, createCache, RspcProvider } from '@sd/client';
|
||||
import { RspcProvider } from '@sd/client';
|
||||
import {
|
||||
createRoutes,
|
||||
Platform,
|
||||
|
@ -88,9 +88,7 @@ const queryClient = new QueryClient({
|
|||
}
|
||||
});
|
||||
|
||||
const cache = createCache();
|
||||
|
||||
const routes = createRoutes(platform, cache);
|
||||
const routes = createRoutes(platform);
|
||||
|
||||
function App() {
|
||||
const router = useRouter();
|
||||
|
@ -114,18 +112,16 @@ function App() {
|
|||
<RspcProvider queryClient={queryClient}>
|
||||
<PlatformProvider platform={platform}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CacheProvider cache={cache}>
|
||||
<SpacedriveInterfaceRoot>
|
||||
<SpacedriveRouterProvider
|
||||
routing={{
|
||||
...router,
|
||||
tabId: '',
|
||||
routes,
|
||||
visible: true
|
||||
}}
|
||||
/>
|
||||
</SpacedriveInterfaceRoot>
|
||||
</CacheProvider>
|
||||
<SpacedriveInterfaceRoot>
|
||||
<SpacedriveRouterProvider
|
||||
routing={{
|
||||
...router,
|
||||
tabId: '',
|
||||
routes,
|
||||
visible: true
|
||||
}}
|
||||
/>
|
||||
</SpacedriveInterfaceRoot>
|
||||
</QueryClientProvider>
|
||||
</PlatformProvider>
|
||||
</RspcProvider>
|
||||
|
|
|
@ -29,7 +29,6 @@ sd-core-sync = { path = "./crates/sync" }
|
|||
# Spacedrive Sub-crates
|
||||
sd-actors = { version = "0.1.0", path = "../crates/actors" }
|
||||
sd-ai = { path = "../crates/ai", optional = true }
|
||||
sd-cache = { path = "../crates/cache" }
|
||||
sd-cloud-api = { version = "0.1.0", path = "../crates/cloud-api" }
|
||||
sd-crypto = { path = "../crates/crypto", features = [
|
||||
"sys",
|
||||
|
|
|
@ -11,7 +11,6 @@ edition = { workspace = true }
|
|||
[dependencies]
|
||||
# Spacedrive Sub-crates
|
||||
sd-prisma = { path = "../../../crates/prisma" }
|
||||
sd-cache = { path = "../../../crates/cache" }
|
||||
|
||||
prisma-client-rust = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
#![forbid(deprecated_in_future)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]
|
||||
|
||||
use sd_prisma::prisma::{self, file_path, job, label, location, object};
|
||||
use sd_prisma::prisma::{file_path, job, label, location, object};
|
||||
|
||||
// File Path selectables!
|
||||
file_path::select!(file_path_pub_id { pub_id });
|
||||
|
@ -176,13 +176,6 @@ object::include!(object_with_file_paths {
|
|||
}
|
||||
});
|
||||
|
||||
impl sd_cache::Model for object_with_file_paths::file_paths::Data {
|
||||
fn name() -> &'static str {
|
||||
// This is okay because it's a superset of the available fields.
|
||||
prisma::file_path::NAME
|
||||
}
|
||||
}
|
||||
|
||||
// Job selectables!
|
||||
job::select!(job_without_data {
|
||||
id
|
||||
|
|
|
@ -19,7 +19,6 @@ use sd_core_prisma_helpers::{
|
|||
file_path_to_isolate, file_path_to_isolate_with_id, object_with_file_paths,
|
||||
};
|
||||
|
||||
use sd_cache::{CacheNode, Model, NormalisedResult, Reference};
|
||||
use sd_file_ext::kind::ObjectKind;
|
||||
use sd_images::ConvertibleExtension;
|
||||
use sd_media_metadata::MediaMetadata;
|
||||
|
@ -75,21 +74,12 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
pub note: Option<String>,
|
||||
pub date_created: Option<DateTime<FixedOffset>>,
|
||||
pub date_accessed: Option<DateTime<FixedOffset>>,
|
||||
pub file_paths: Vec<Reference<file_path::Data>>,
|
||||
}
|
||||
|
||||
impl Model for ObjectWithFilePaths2 {
|
||||
fn name() -> &'static str {
|
||||
"Object" // is a duplicate because it's the same entity but with a relation
|
||||
}
|
||||
pub file_paths: Vec<object_with_file_paths::file_paths::Data>,
|
||||
}
|
||||
|
||||
impl ObjectWithFilePaths2 {
|
||||
pub fn from_db(
|
||||
nodes: &mut Vec<CacheNode>,
|
||||
item: object_with_file_paths::Data,
|
||||
) -> Reference<Self> {
|
||||
let this = Self {
|
||||
pub fn from_db(item: object_with_file_paths::Data) -> Self {
|
||||
Self {
|
||||
id: item.id,
|
||||
pub_id: item.pub_id,
|
||||
kind: item.kind,
|
||||
|
@ -100,20 +90,8 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
note: item.note,
|
||||
date_created: item.date_created,
|
||||
date_accessed: item.date_accessed,
|
||||
file_paths: item
|
||||
.file_paths
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let id = i.id.to_string();
|
||||
nodes.push(CacheNode::new(id.clone(), i));
|
||||
Reference::new(id)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let id = this.id.to_string();
|
||||
nodes.push(CacheNode::new(id.clone(), this));
|
||||
Reference::new(id)
|
||||
file_paths: item.file_paths,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,13 +104,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.include(object_with_file_paths::include())
|
||||
.exec()
|
||||
.await?
|
||||
.map(|item| {
|
||||
let mut nodes = Vec::new();
|
||||
NormalisedResult {
|
||||
item: ObjectWithFilePaths2::from_db(&mut nodes, item),
|
||||
nodes,
|
||||
}
|
||||
}))
|
||||
.map(|item| ObjectWithFilePaths2::from_db(item)))
|
||||
})
|
||||
})
|
||||
.procedure("getMediaData", {
|
||||
|
|
|
@ -8,7 +8,6 @@ use crate::{
|
|||
|
||||
use futures::StreamExt;
|
||||
use prisma_client_rust::raw;
|
||||
use sd_cache::{Model, Normalise, NormalisedResult, NormalisedResults};
|
||||
use sd_file_ext::kind::ObjectKind;
|
||||
use sd_p2p::RemoteIdentity;
|
||||
use sd_prisma::prisma::{indexer_rule, object, statistics};
|
||||
|
@ -57,12 +56,6 @@ pub struct LibraryConfigWrapped {
|
|||
pub config: LibraryConfig,
|
||||
}
|
||||
|
||||
impl Model for LibraryConfigWrapped {
|
||||
fn name() -> &'static str {
|
||||
"LibraryConfigWrapped"
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryConfigWrapped {
|
||||
pub async fn from_library(library: &Library) -> Self {
|
||||
Self {
|
||||
|
@ -78,7 +71,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
R.router()
|
||||
.procedure("list", {
|
||||
R.query(|node, _: ()| async move {
|
||||
let libraries = node
|
||||
Ok(node
|
||||
.libraries
|
||||
.get_all()
|
||||
.await
|
||||
|
@ -93,11 +86,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join()
|
||||
.await;
|
||||
|
||||
let (nodes, items) = libraries.normalise(|i| i.uuid.to_string());
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
.await)
|
||||
})
|
||||
})
|
||||
.procedure("statistics", {
|
||||
|
@ -318,10 +307,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.await?;
|
||||
}
|
||||
|
||||
Ok(NormalisedResult::from(
|
||||
LibraryConfigWrapped::from_library(&library).await,
|
||||
|l| l.uuid.to_string(),
|
||||
))
|
||||
Ok(LibraryConfigWrapped::from_library(&library).await)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
@ -16,7 +16,6 @@ use sd_core_prisma_helpers::{
|
|||
file_path_with_object, label_with_objects, location_with_indexer_rules, object_with_file_paths,
|
||||
};
|
||||
|
||||
use sd_cache::{CacheNode, Model, Normalise, NormalisedResult, NormalisedResults, Reference};
|
||||
use sd_prisma::prisma::{file_path, indexer_rule, indexer_rules_in_location, location, SortOrder};
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -67,14 +66,6 @@ pub enum ExplorerItem {
|
|||
},
|
||||
}
|
||||
|
||||
// TODO: Really this shouldn't be a `Model` but it's easy for now.
|
||||
// In the future we should store the inner data of the variant on behalf of it's existing model so it works cross queries.
|
||||
impl Model for ExplorerItem {
|
||||
fn name() -> &'static str {
|
||||
"ExplorerItem"
|
||||
}
|
||||
}
|
||||
|
||||
impl ExplorerItem {
|
||||
pub fn id(&self) -> String {
|
||||
let ty = match self {
|
||||
|
@ -207,17 +198,13 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
R.router()
|
||||
.procedure("list", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
let locations = library
|
||||
Ok(library
|
||||
.db
|
||||
.location()
|
||||
.find_many(vec![])
|
||||
.order_by(location::date_created::order(SortOrder::Desc))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
let (nodes, items) = locations.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { items, nodes })
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.procedure("get", {
|
||||
|
@ -228,8 +215,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.location()
|
||||
.find_unique(location::id::equals(location_id))
|
||||
.exec()
|
||||
.await?
|
||||
.map(|i| NormalisedResult::from(i, |i| i.id.to_string())))
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.procedure("getWithRules", {
|
||||
|
@ -248,21 +234,12 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
pub hidden: Option<bool>,
|
||||
pub date_created: Option<DateTime<FixedOffset>>,
|
||||
pub instance_id: Option<i32>,
|
||||
pub indexer_rules: Vec<Reference<indexer_rule::Data>>,
|
||||
}
|
||||
|
||||
impl Model for LocationWithIndexerRule {
|
||||
fn name() -> &'static str {
|
||||
"Location" // This is a duplicate identifier as `location::Data` but it's fine because because they are the same entity
|
||||
}
|
||||
pub indexer_rules: Vec<indexer_rule::Data>,
|
||||
}
|
||||
|
||||
impl LocationWithIndexerRule {
|
||||
pub fn from_db(
|
||||
nodes: &mut Vec<CacheNode>,
|
||||
value: location_with_indexer_rules::Data,
|
||||
) -> Reference<Self> {
|
||||
let this = Self {
|
||||
pub fn from_db(value: location_with_indexer_rules::Data) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
pub_id: value.pub_id,
|
||||
name: value.name,
|
||||
|
@ -279,18 +256,9 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
indexer_rules: value
|
||||
.indexer_rules
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let id = i.indexer_rule.id.to_string();
|
||||
|
||||
nodes.push(CacheNode::new(id.clone(), i.indexer_rule));
|
||||
Reference::new(id)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let id = this.id.to_string();
|
||||
nodes.push(CacheNode::new(id.clone(), this));
|
||||
Reference::new(id)
|
||||
.map(|i| i.indexer_rule)
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,13 +271,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.include(location_with_indexer_rules::include())
|
||||
.exec()
|
||||
.await?
|
||||
.map(|location| {
|
||||
let mut nodes = Vec::new();
|
||||
NormalisedResult {
|
||||
item: LocationWithIndexerRule::from_db(&mut nodes, location),
|
||||
nodes,
|
||||
}
|
||||
}))
|
||||
.map(|location| LocationWithIndexerRule::from_db(location)))
|
||||
})
|
||||
})
|
||||
.procedure("create", {
|
||||
|
@ -587,34 +549,25 @@ fn mount_indexer_rule_routes() -> AlphaRouter<Ctx> {
|
|||
format!("Indexer rule <id={indexer_rule_id}> not found"),
|
||||
)
|
||||
})
|
||||
.map(|i| NormalisedResult::from(i, |i| i.id.to_string()))
|
||||
})
|
||||
})
|
||||
.procedure("list", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
let rules = library.db.indexer_rule().find_many(vec![]).exec().await?;
|
||||
|
||||
let (nodes, items) = rules.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { items, nodes })
|
||||
Ok(library.db.indexer_rule().find_many(vec![]).exec().await?)
|
||||
})
|
||||
})
|
||||
// list indexer rules for location, returning the indexer rule
|
||||
.procedure("listForLocation", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), location_id: location::id::Type| async move {
|
||||
let rules = library
|
||||
Ok(library
|
||||
.db
|
||||
.indexer_rule()
|
||||
.find_many(vec![indexer_rule::locations::some(vec![
|
||||
indexer_rules_in_location::location_id::equals(location_id),
|
||||
])])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
let (nodes, items) = rules.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { items, nodes })
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ use crate::{
|
|||
Node,
|
||||
};
|
||||
|
||||
use sd_cache::patch_typedef;
|
||||
use sd_p2p::RemoteIdentity;
|
||||
use sd_prisma::prisma::file_path;
|
||||
|
||||
|
@ -210,8 +209,6 @@ pub(crate) fn mount() -> Arc<Router> {
|
|||
.merge("backups.", backups::mount())
|
||||
.merge("invalidation.", utils::mount_invalidate())
|
||||
.sd_patch_types_dangerously(|type_map| {
|
||||
patch_typedef(type_map);
|
||||
|
||||
let def =
|
||||
<sd_prisma::prisma::object::Data as specta::NamedType>::definition_named_data_type(
|
||||
type_map,
|
||||
|
|
|
@ -6,10 +6,12 @@ use crate::{
|
|||
util::{unsafe_streamed_query, BatchedStream},
|
||||
};
|
||||
|
||||
use sd_core_indexer_rules::seed::no_hidden;
|
||||
use sd_core_indexer_rules::IndexerRule;
|
||||
use sd_core_prisma_helpers::{file_path_with_object, object_with_file_paths};
|
||||
|
||||
use sd_cache::{CacheNode, Model, Normalise, Reference};
|
||||
use sd_prisma::prisma::{self, PrismaClient};
|
||||
use sd_file_ext::kind::ObjectKind;
|
||||
use sd_prisma::prisma::{self, location, PrismaClient};
|
||||
use sd_utils::chain_optional_iter;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
@ -33,16 +35,9 @@ use super::{Ctx, R};
|
|||
const MAX_TAKE: u8 = 100;
|
||||
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
struct SearchData<T: Model> {
|
||||
struct SearchData<T> {
|
||||
cursor: Option<Vec<u8>>,
|
||||
items: Vec<Reference<T>>,
|
||||
nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
impl<T: Model> Model for SearchData<T> {
|
||||
fn name() -> &'static str {
|
||||
T::name()
|
||||
}
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug, Clone)]
|
||||
|
@ -89,9 +84,8 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
}
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
struct EphemeralPathsResultItem {
|
||||
pub entries: Vec<Reference<ExplorerItem>>,
|
||||
pub entries: Vec<ExplorerItem>,
|
||||
pub errors: Vec<rspc::Error>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
R.with2(library()).subscription(
|
||||
|
@ -154,12 +148,9 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
}
|
||||
}
|
||||
|
||||
let (nodes, entries) = entries.normalise(|item: &ExplorerItem| item.id());
|
||||
|
||||
yield EphemeralPathsResultItem {
|
||||
entries,
|
||||
errors,
|
||||
nodes,
|
||||
};
|
||||
}
|
||||
}))
|
||||
|
@ -249,12 +240,9 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
}
|
||||
|
||||
let (nodes, items) = items.normalise(|item| item.id());
|
||||
|
||||
Ok(SearchData {
|
||||
items,
|
||||
cursor: None,
|
||||
nodes,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
@ -370,13 +358,7 @@ pub fn mount() -> AlphaRouter<Ctx> {
|
|||
});
|
||||
}
|
||||
|
||||
let (nodes, items) = items.normalise(|item| item.id());
|
||||
|
||||
Ok(SearchData {
|
||||
nodes,
|
||||
items,
|
||||
cursor,
|
||||
})
|
||||
Ok(SearchData { items, cursor })
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use crate::{invalidate_query, library::Library, object::tag::TagCreateArgs};
|
||||
|
||||
use sd_cache::{CacheNode, Normalise, NormalisedResult, NormalisedResults, Reference};
|
||||
use sd_prisma::{
|
||||
prisma::{file_path, object, tag, tag_on_object},
|
||||
prisma_sync,
|
||||
|
@ -23,40 +22,26 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
R.router()
|
||||
.procedure("list", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
let tags = library.db.tag().find_many(vec![]).exec().await?;
|
||||
|
||||
let (nodes, items) = tags.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
Ok(library.db.tag().find_many(vec![]).exec().await?)
|
||||
})
|
||||
})
|
||||
.procedure("getForObject", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), object_id: i32| async move {
|
||||
let tags = library
|
||||
Ok(library
|
||||
.db
|
||||
.tag()
|
||||
.find_many(vec![tag::tag_objects::some(vec![
|
||||
tag_on_object::object_id::equals(object_id),
|
||||
])])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
let (nodes, items) = tags.normalise(|i| i.id.to_string());
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.procedure("getWithObjects", {
|
||||
#[derive(Serialize, Type)]
|
||||
pub struct GetWithObjectsResult {
|
||||
pub data: BTreeMap<u32, Vec<Reference<tag::Data>>>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Type)]
|
||||
pub struct ObjectWithDateCreated {
|
||||
object: Reference<object::Data>,
|
||||
object: object::Data,
|
||||
date_created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
@ -97,8 +82,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
.tag()
|
||||
.find_unique(tag::id::equals(tag_id))
|
||||
.exec()
|
||||
.await?
|
||||
.map(|tag| NormalisedResult::from(tag, |i| i.id.to_string())))
|
||||
.await?)
|
||||
})
|
||||
})
|
||||
.procedure("create", {
|
||||
|
|
|
@ -1,29 +1,11 @@
|
|||
use crate::volume::get_volumes;
|
||||
|
||||
use sd_cache::{Normalise, NormalisedResults};
|
||||
|
||||
use rspc::alpha::AlphaRouter;
|
||||
|
||||
use super::{Ctx, R};
|
||||
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router().procedure("list", {
|
||||
R.query(|_, _: ()| async move {
|
||||
let volumes = get_volumes().await;
|
||||
|
||||
let (nodes, items) = volumes.normalise(|i| {
|
||||
// TODO: This is a really bad key. Once we hook up volumes with the DB fix this!
|
||||
blake3::hash(
|
||||
&i.mount_points
|
||||
.iter()
|
||||
.flat_map(|mp| mp.as_os_str().to_string_lossy().as_bytes().to_vec())
|
||||
.collect::<Vec<u8>>(),
|
||||
)
|
||||
.to_hex()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
Ok(NormalisedResults { nodes, items })
|
||||
})
|
||||
R.query(|_, _: ()| async move { Ok(get_volumes().await) })
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// Adapted from: https://github.com/kimlimjustin/xplorer/blob/f4f3590d06783d64949766cc2975205a3b689a56/src-tauri/src/drives.rs
|
||||
use sd_cache::Model;
|
||||
|
||||
use std::{
|
||||
fmt::Display,
|
||||
|
@ -57,12 +56,6 @@ pub struct Volume {
|
|||
pub is_root_filesystem: bool,
|
||||
}
|
||||
|
||||
impl Model for Volume {
|
||||
fn name() -> &'static str {
|
||||
"Volume"
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Volume {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
|
|
11
crates/cache/Cargo.toml
vendored
11
crates/cache/Cargo.toml
vendored
|
@ -1,11 +0,0 @@
|
|||
[package]
|
||||
name = "sd-cache"
|
||||
version = "0.0.0"
|
||||
license = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
specta = { workspace = true }
|
229
crates/cache/src/lib.rs
vendored
229
crates/cache/src/lib.rs
vendored
|
@ -1,229 +0,0 @@
|
|||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde::{ser::SerializeMap, Serialize, Serializer};
|
||||
use specta::{Any, DataType, NamedType, Type, TypeMap};
|
||||
|
||||
/// A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
///
|
||||
/// You don't need to use this, it's just a shortcut to avoid having to write out the full type every time.
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
pub struct NormalisedResults<T: Model + Type> {
|
||||
pub items: Vec<Reference<T>>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
/// A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
///
|
||||
/// You don't need to use this, it's just a shortcut to avoid having to write out the full type every time.
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
pub struct NormalisedResult<T: Model + Type> {
|
||||
pub item: Reference<T>,
|
||||
pub nodes: Vec<CacheNode>,
|
||||
}
|
||||
|
||||
impl<T: Model + Serialize + Type> NormalisedResult<T> {
|
||||
pub fn from(item: T, id_fn: impl Fn(&T) -> String) -> Self {
|
||||
let id = id_fn(&item);
|
||||
Self {
|
||||
item: Reference::new(id.clone()),
|
||||
nodes: vec![CacheNode::new(id, item)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type which can be stored in the cache.
|
||||
pub trait Model {
|
||||
/// Must return a unique identifier for this model within the cache.
|
||||
fn name() -> &'static str;
|
||||
}
|
||||
|
||||
/// A reference to a `CacheNode`.
|
||||
///
|
||||
/// This does not contain the actual data, but instead a reference to it.
|
||||
/// This allows the CacheNode's to be switched out and the query recomputed without any backend communication.
|
||||
///
|
||||
/// If you use a `Reference` in a query, you *must* ensure the corresponding `CacheNode` is also in the query.
|
||||
#[derive(Type, Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Reference<T> {
|
||||
__type: &'static str,
|
||||
__id: String,
|
||||
#[specta(rename = "#type")]
|
||||
ty: PhantomType<T>,
|
||||
}
|
||||
|
||||
impl<T: Model + Type> Reference<T> {
|
||||
pub fn new(key: String) -> Self {
|
||||
Self {
|
||||
__type: "", // This is just to fake the field for Specta
|
||||
__id: key,
|
||||
ty: PhantomType(PhantomData),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Model> Serialize for Reference<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
map.serialize_entry("__type", T::name())?;
|
||||
map.serialize_entry("__id", &self.__id)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in the cache.
|
||||
/// This holds the data and is identified by it's type and id.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheNode(
|
||||
&'static str,
|
||||
serde_json::Value,
|
||||
Result<serde_json::Value, Arc<serde_json::Error>>,
|
||||
);
|
||||
|
||||
impl CacheNode {
|
||||
pub fn new<T: Model + Serialize + Type>(key: String, value: T) -> Self {
|
||||
Self(
|
||||
T::name(),
|
||||
key.into(),
|
||||
serde_json::to_value(value).map_err(Arc::new),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CacheNode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
&& self.1 == other.1
|
||||
&& match (&self.2, &other.2) {
|
||||
(Ok(v0), Ok(v1)) => v0 == v1,
|
||||
// Compares the values in the Arcs, not the Arc objects themselves.
|
||||
(Err(e0), Err(e1)) => {
|
||||
(*e0).classify() == (*e1).classify()
|
||||
&& (*e0).column() == (*e1).column()
|
||||
&& (*e0).line() == (*e1).line()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for CacheNode {}
|
||||
|
||||
impl Hash for CacheNode {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
self.1.as_str().hash(state);
|
||||
self.1.as_str().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Type, Default)]
|
||||
#[specta(rename = "CacheNode", remote = CacheNode)]
|
||||
#[allow(unused)]
|
||||
struct CacheNodeTy {
|
||||
__type: String,
|
||||
__id: String,
|
||||
#[specta(rename = "#node")]
|
||||
node: Any,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NodeSerdeRepr<'a> {
|
||||
__type: &'static str,
|
||||
__id: &'a serde_json::Value,
|
||||
#[serde(flatten)]
|
||||
v: &'a serde_json::Value,
|
||||
}
|
||||
|
||||
impl Serialize for CacheNode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
NodeSerdeRepr {
|
||||
__type: self.0,
|
||||
__id: &self.1,
|
||||
v: self.2.as_ref().map_err(|err| {
|
||||
serde::ser::Error::custom(format!("Failed to serialize node: {}", err))
|
||||
})?,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for easily normalizing data.
|
||||
pub trait Normalise {
|
||||
type Item: Model + Type;
|
||||
|
||||
fn normalise(
|
||||
self,
|
||||
id_fn: impl Fn(&Self::Item) -> String,
|
||||
) -> (Vec<CacheNode>, Vec<Reference<Self::Item>>);
|
||||
}
|
||||
|
||||
impl<T: Model + Serialize + Type> Normalise for Vec<T> {
|
||||
type Item = T;
|
||||
|
||||
fn normalise(
|
||||
self,
|
||||
id_fn: impl Fn(&Self::Item) -> String,
|
||||
) -> (Vec<CacheNode>, Vec<Reference<Self::Item>>) {
|
||||
let mut nodes = Vec::with_capacity(self.len());
|
||||
let mut references = Vec::with_capacity(self.len());
|
||||
|
||||
for item in self.into_iter() {
|
||||
let id = id_fn(&item);
|
||||
nodes.push(CacheNode::new(id.clone(), item));
|
||||
references.push(Reference::new(id));
|
||||
}
|
||||
|
||||
(nodes, references)
|
||||
}
|
||||
}
|
||||
|
||||
/// Basically `PhantomData`.
|
||||
///
|
||||
/// With Specta `PhantomData` is exported as `null`.
|
||||
/// This will export as `T` but serve the same purpose as `PhantomData` (holding a type without it being instantiated).
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct PhantomType<T>(PhantomData<T>);
|
||||
|
||||
/// WARNING: This type is surgically updated within `Reference` in the final typedefs due it being impossible to properly implement.
|
||||
/// Be careful changing it!
|
||||
|
||||
impl<T: Type> Type for PhantomType<T> {
|
||||
fn inline(type_map: &mut TypeMap, generics: &[DataType]) -> DataType {
|
||||
T::inline(type_map, generics)
|
||||
}
|
||||
|
||||
fn reference(type_map: &mut TypeMap, generics: &[DataType]) -> specta::reference::Reference {
|
||||
T::reference(type_map, generics)
|
||||
}
|
||||
|
||||
fn definition(type_map: &mut TypeMap) -> DataType {
|
||||
T::definition(type_map)
|
||||
}
|
||||
}
|
||||
|
||||
// This function is cursed.
|
||||
pub fn patch_typedef(type_map: &mut TypeMap) {
|
||||
#[derive(Type)]
|
||||
#[specta(rename = "Reference")]
|
||||
#[allow(unused)]
|
||||
struct ReferenceTy<T> {
|
||||
__type: &'static str,
|
||||
__id: String,
|
||||
#[specta(rename = "#type")]
|
||||
ty: T,
|
||||
}
|
||||
|
||||
let mut def = <Reference<()> as NamedType>::definition_named_data_type(type_map);
|
||||
def.inner = ReferenceTy::<Any>::definition(type_map);
|
||||
type_map.insert(<Reference<()> as NamedType>::SID, def)
|
||||
}
|
|
@ -5,7 +5,6 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
# Spacedrive Sub-crates
|
||||
sd-cache = { path = "../cache" }
|
||||
sd-sync = { path = "../sync" }
|
||||
|
||||
prisma-client-rust = { workspace = true }
|
||||
|
|
|
@ -3,22 +3,6 @@ pub mod prisma;
|
|||
#[allow(warnings, unused)]
|
||||
pub mod prisma_sync;
|
||||
|
||||
macro_rules! impl_model {
|
||||
($module:ident) => {
|
||||
impl sd_cache::Model for prisma::$module::Data {
|
||||
fn name() -> &'static str {
|
||||
prisma::$module::NAME
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_model!(tag);
|
||||
impl_model!(object);
|
||||
impl_model!(location);
|
||||
impl_model!(indexer_rule);
|
||||
impl_model!(file_path);
|
||||
|
||||
pub async fn test_db() -> std::sync::Arc<prisma::PrismaClient> {
|
||||
std::sync::Arc::new(
|
||||
prisma::PrismaClient::_builder()
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
---
|
||||
title: Normalised Cache
|
||||
index: 12
|
||||
---
|
||||
|
||||
# Normalised Cache
|
||||
|
||||
We use a normalised cache for our frontend to ensure the UI will always contain consistent data.
|
||||
|
||||
## What this system does?
|
||||
|
||||
By normalising the data it's impossible to get "state tearing".
|
||||
|
||||
Each time `useNodes` or `cache.withNodes` is called all `useCache` hooks will reexecute if they depend on a node that has changed.
|
||||
|
||||
This means the queries will always render the newest version of the model.
|
||||
|
||||
## Terminology
|
||||
|
||||
- `CacheNode`: A node in the cache - this contains the data and can be identified by the model's name and unique ID within the data (eg. database primary key).
|
||||
- `Reference<T>`: A reference to a node in the cache - This contains the model's name and unique ID.
|
||||
|
||||
## High level overview
|
||||
|
||||
We turn the data on the backend into a list of `CacheNode`'s and a list of `Reference<T>`'s and then return it to the frontend.
|
||||
|
||||
We insert the `CacheNode`'s into a global cache on the frontend and then use the `Reference<T>`'s to reconstruct the data by looking up the `CacheNode`'s.
|
||||
|
||||
When the cache changes (from another query, invalidation, etc), we can reconstruct _all_ queries using their `Reference<T>`'s to reflect the updated data.
|
||||
|
||||
## Rust usage
|
||||
|
||||
The Rust helpers are defined [here](https://github.com/spacedriveapp/spacedrive/blob/main/crates/cache/src/lib.rs) and can be used like the following:
|
||||
|
||||
```rust
|
||||
pub struct Demo {
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl sd_cache::Model for Demo {
|
||||
// The name + the ID *must* refer to a unique node.
|
||||
// If your using an enum, the variant should show up in the ID (although this isn't possible right now)
|
||||
fn name() -> &'static str {
|
||||
"Demo"
|
||||
}
|
||||
}
|
||||
|
||||
let data: Vec<Demo> = vec![];
|
||||
|
||||
// We normalised the data but splitting it into a group of reference and a group of `CacheNode`'s.
|
||||
let (nodes, items) = libraries.normalise(|i| i.id);
|
||||
|
||||
// `NormalisedResults` or `NormalisedResult` are optional wrapper types to hold a one or multiple items and their cache nodes.
|
||||
// You don't have to use them, but they save declaring a bunch of identical structs.
|
||||
//
|
||||
// Alternatively add `nodes: Vec<CacheNode>` and `items: Vec<Reference<T>>` to your existing return type.
|
||||
//
|
||||
return sd_cache::NormalisedResults { nodes, items };
|
||||
```
|
||||
|
||||
## Typescript usage
|
||||
|
||||
The Typescript helpers are defined [here](https://github.com/spacedriveapp/spacedrive/blob/main/packages/client/src/cache.tsx).
|
||||
|
||||
### Usage with React
|
||||
|
||||
We have helpers designed for easy usage within React's lifecycle.
|
||||
|
||||
```ts
|
||||
const query = useLibraryQuery([...]);
|
||||
|
||||
// This will inject all the models into the cache
|
||||
useNodes(query.data?.nodes);
|
||||
|
||||
// This will reconstruct the data from the cache
|
||||
const data = useCache(query.data?.item);
|
||||
|
||||
console.log(data);
|
||||
```
|
||||
|
||||
### Vanilla JS
|
||||
|
||||
These API's are really useful for special cases. In general aim to use the React API's unless you have a good reason for these.
|
||||
|
||||
```ts
|
||||
const cache = useNormalisedCache(); // Get the cache within the react context
|
||||
|
||||
// Pass `cache` outside React (Eg. `useEffect`, `onSuccess`, etc)
|
||||
|
||||
const data = ...;
|
||||
|
||||
// This will inject all the models into the cache
|
||||
cache.withNodes(data.nodes)
|
||||
|
||||
// This will reconstruct the data from the cache
|
||||
//
|
||||
// *WARNING* This is not reactive. So any changes to the nodes will not be reflected.
|
||||
// Using this is fine if you need to quickly check the data but don't hold onto it.
|
||||
const data = useCache(query.data?.item);
|
||||
|
||||
console.log(data);
|
||||
```
|
||||
|
||||
## Design decisions
|
||||
|
||||
### Why `useNodes` and `useCache`?
|
||||
|
||||
This was done to make the system super flexible with what data you can return from your backend.
|
||||
|
||||
For example the backend doesn't just have to return `NormalisedResults` or `NormalisedResult`, it could return:
|
||||
|
||||
```rust
|
||||
pub struct AllTheData {
|
||||
file_paths: Vec<Reference<FilePath>>,
|
||||
locations: Vec<Reference<Location>>,
|
||||
nodes: Vec<CacheNode>
|
||||
}
|
||||
```
|
||||
|
||||
and then on the frontend you could do the following:
|
||||
|
||||
```ts
|
||||
const query = useQuery([...]);
|
||||
useNodes(query.data?.nodes);
|
||||
const locations = useCache(query.data?.locations);
|
||||
const filePaths = useCache(query.data?.file_paths);
|
||||
```
|
||||
|
||||
This is only possible because `useNodes` and `useCache` take in a specific key, instead of the whole `data` object, so you can tell it where to look.
|
||||
|
||||
## Known issues
|
||||
|
||||
### Specta support
|
||||
|
||||
Expressing `Reference<T>` in Specta is really hard so we [surgically update](https://github.com/spacedriveapp/spacedrive/blob/a315dd632da8175b47f9e0713d3c7fc470329352/core/src/api/mod.rs#L219) it's [type definition](https://github.com/spacedriveapp/spacedrive/blob/a315dd632da8175b47f9e0713d3c7fc470329352/crates/cache/src/lib.rs#L215).
|
||||
|
||||
This is done using `rspc::Router::sd_patch_types_dangerously` which is a method specific to our fork [spacedrive/rspc](https://github.com/spacedriveapp/rspc).
|
||||
|
||||
### Invalidation system integration
|
||||
|
||||
The initial implementation of this idea with an MVP. It works with the existing invalidation system like regular queries, but the invalidation system isn't aware of the normalised cache like a better implementation would be.
|
|
@ -4,7 +4,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
|||
import clsx from 'clsx';
|
||||
import { RefObject, useMemo, useRef } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ExplorerItem, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { ExplorerItem, useLibraryQuery } from '@sd/client';
|
||||
import { Button, dialogManager, ModifierKeys, tw } from '@sd/ui';
|
||||
import CreateDialog, {
|
||||
AssignTagItems,
|
||||
|
@ -23,7 +23,6 @@ interface Props {
|
|||
|
||||
function useData({ items }: Props) {
|
||||
const tags = useLibraryQuery(['tags.list'], { suspense: true });
|
||||
useNodes(tags.data?.nodes);
|
||||
|
||||
// Map<tag::id, Vec<object::id>>
|
||||
const tagsWithObjects = useLibraryQuery(
|
||||
|
@ -42,7 +41,7 @@ function useData({ items }: Props) {
|
|||
return {
|
||||
tags: {
|
||||
...tags,
|
||||
data: useCache(tags.data?.items)
|
||||
data: tags.data
|
||||
},
|
||||
tagsWithObjects
|
||||
};
|
||||
|
|
|
@ -37,10 +37,8 @@ import {
|
|||
ObjectKindEnum,
|
||||
ObjectWithFilePaths,
|
||||
useBridgeQuery,
|
||||
useCache,
|
||||
useItemsAsObjects,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useSelector,
|
||||
type ExplorerItem
|
||||
} from '@sd/client';
|
||||
|
@ -180,8 +178,7 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
|||
const { t } = useLocale();
|
||||
|
||||
const result = useLibraryQuery(['locations.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const locations = useCache(result.data?.items);
|
||||
const locations = result.data || [];
|
||||
|
||||
switch (item.type) {
|
||||
case 'NonIndexedPath': {
|
||||
|
@ -225,8 +222,7 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
|||
const tagsQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
|
||||
enabled: objectData != null && readyToFetch
|
||||
});
|
||||
useNodes(tagsQuery.data?.nodes);
|
||||
const tags = useCache(tagsQuery.data?.items);
|
||||
const tags = tagsQuery.data;
|
||||
|
||||
// const labels = useLibraryQuery(['labels.getForObject', objectData?.id ?? -1], {
|
||||
// enabled: objectData != null && readyToFetch
|
||||
|
@ -434,8 +430,7 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
|
|||
enabled: readyToFetch && !isDragSelecting,
|
||||
suspense: true
|
||||
});
|
||||
useNodes(tagsQuery.data?.nodes);
|
||||
const tags = useCache(tagsQuery.data?.items);
|
||||
const tags = tagsQuery.data;
|
||||
|
||||
// const labels = useLibraryQuery(['labels.list'], {
|
||||
// enabled: readyToFetch && !isDragSelecting,
|
||||
|
|
|
@ -6,14 +6,7 @@ import { useEffect, useState } from 'react';
|
|||
import CommandPalette, { filterItems, getItemIndex } from 'react-cmdk';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
arraysEqual,
|
||||
useCache,
|
||||
useLibraryContext,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import { arraysEqual, useLibraryContext, useLibraryQuery, useOnlineLocations } from '@sd/client';
|
||||
import { dialogManager } from '@sd/ui';
|
||||
import i18n from '~/app/I18n';
|
||||
import { Icon } from '~/components';
|
||||
|
@ -58,8 +51,7 @@ const CMDK = () => {
|
|||
const [search, setSearch] = useState('');
|
||||
|
||||
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
|
||||
const onlineLocations = useOnlineLocations();
|
||||
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import clsx from 'clsx';
|
||||
import CommandPalette from 'react-cmdk';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { arraysEqual, useCache, useLibraryQuery, useNodes, useOnlineLocations } from '@sd/client';
|
||||
import { arraysEqual, useLibraryQuery, useOnlineLocations } from '@sd/client';
|
||||
import { Icon } from '~/components';
|
||||
|
||||
export default function CMDKLocations() {
|
||||
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
|
||||
const onlineLocations = useOnlineLocations();
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import CommandPalette from 'react-cmdk';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useCache, useLibraryQuery, useNodes, type Tag } from '@sd/client';
|
||||
import { useLibraryQuery, type Tag } from '@sd/client';
|
||||
|
||||
export default function CMDKTags() {
|
||||
const result = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const tags = useCache(result.data?.items);
|
||||
const tags = result.data || [];
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ArrowRight, EjectSimple } from '@phosphor-icons/react';
|
||||
import clsx from 'clsx';
|
||||
import { PropsWithChildren, useMemo } from 'react';
|
||||
import { useBridgeQuery, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { Button, toast, tw } from '@sd/ui';
|
||||
import { Icon, IconName } from '~/components';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
@ -34,13 +34,11 @@ const SidebarIcon = ({ name }: { name: IconName }) => {
|
|||
export default function LocalSection() {
|
||||
const platform = usePlatform();
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
|
||||
const homeDir = useHomeDir();
|
||||
const result = useBridgeQuery(['volumes.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const volumes = useCache(result.data?.items);
|
||||
const volumes = result.data;
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
|
|
|
@ -3,9 +3,7 @@ import { Link, useMatch } from 'react-router-dom';
|
|||
import {
|
||||
arraysEqual,
|
||||
Location as LocationType,
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import { useExplorerDroppable } from '~/app/$libraryId/Explorer/useExplorerDroppable';
|
||||
|
@ -21,8 +19,7 @@ import { ContextMenu } from './ContextMenu';
|
|||
|
||||
export default function Locations() {
|
||||
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
const onlineLocations = useOnlineLocations();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
import { NavLink, useMatch } from 'react-router-dom';
|
||||
import { useCache, useLibraryQuery, useNodes, type Tag } from '@sd/client';
|
||||
import { useLibraryQuery, type Tag } from '@sd/client';
|
||||
import { useExplorerDroppable } from '~/app/$libraryId/Explorer/useExplorerDroppable';
|
||||
import { SubtleButton } from '~/components';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
@ -12,8 +12,7 @@ import { ContextMenu } from './ContextMenu';
|
|||
|
||||
export default function TagsSection() {
|
||||
const result = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const tags = useCache(result.data?.items);
|
||||
const tags = result.data;
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { snapshot } from 'valtio';
|
||||
import { useNormalisedCache } from '@sd/client';
|
||||
|
||||
export function Component() {
|
||||
const cache = useNormalisedCache();
|
||||
const data = snapshot(cache['#cache']);
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1>Cache Debug</h1>
|
||||
<pre className="pt-4">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { RouteObject } from 'react-router';
|
||||
|
||||
export const debugRoutes = [
|
||||
{ path: 'cache', lazy: () => import('./cache') },
|
||||
{ path: 'cloud', lazy: () => import('./cloud') },
|
||||
{ path: 'sync', lazy: () => import('./sync') },
|
||||
{ path: 'actors', lazy: () => import('./actors') },
|
||||
|
|
|
@ -3,10 +3,8 @@ import { Outlet, useNavigate } from 'react-router';
|
|||
import {
|
||||
useBridgeMutation,
|
||||
useBridgeQuery,
|
||||
useCache,
|
||||
useConnectedPeers,
|
||||
useDiscoveredPeers,
|
||||
useNodes
|
||||
useDiscoveredPeers
|
||||
} from '@sd/client';
|
||||
import { Button, toast } from '@sd/ui';
|
||||
import { useZodRouteParams, useZodSearchParams } from '~/hooks';
|
||||
|
@ -45,8 +43,7 @@ export function Overview() {
|
|||
const result = useBridgeQuery(['library.list']);
|
||||
const connectedPeers = useConnectedPeers();
|
||||
const discoveredPeers = useDiscoveredPeers();
|
||||
useNodes(result.data?.nodes);
|
||||
const libraries = useCache(result.data?.items);
|
||||
const libraries = result.data || [];
|
||||
const debugConnect = useBridgeMutation(['p2p.debugConnect'], {
|
||||
onSuccess: () => {
|
||||
toast.success('Connected!');
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
ExplorerItem,
|
||||
getExplorerItemData,
|
||||
useLibraryContext,
|
||||
useNormalisedCache,
|
||||
useUnsafeStreamedQuery,
|
||||
type EphemeralPathOrder
|
||||
} from '@sd/client';
|
||||
|
@ -181,7 +180,6 @@ const EphemeralExplorer = memo((props: { args: PathParams }) => {
|
|||
const settingsSnapshot = explorerSettings.useSettingsSnapshot();
|
||||
|
||||
const libraryCtx = useLibraryContext();
|
||||
const cache = useNormalisedCache();
|
||||
const query = useUnsafeStreamedQuery(
|
||||
[
|
||||
'search.ephemeralPaths',
|
||||
|
@ -198,18 +196,16 @@ const EphemeralExplorer = memo((props: { args: PathParams }) => {
|
|||
enabled: path != null,
|
||||
suspense: true,
|
||||
onSuccess: () => explorerStore.resetCache(),
|
||||
onBatch: (item) => {
|
||||
cache.withNodes(item.nodes);
|
||||
}
|
||||
onBatch: (item) => {}
|
||||
}
|
||||
);
|
||||
|
||||
const entries = useMemo(() => {
|
||||
return cache.withCache(
|
||||
return (
|
||||
query.data?.flatMap((item) => item.entries) ||
|
||||
query.streaming.flatMap((item) => item.entries)
|
||||
query.streaming.flatMap((item) => item.entries)
|
||||
);
|
||||
}, [cache, query.streaming, query.data]);
|
||||
}, [query.streaming, query.data]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!entries) return [];
|
||||
|
|
|
@ -5,12 +5,10 @@ import {
|
|||
arraysEqual,
|
||||
FilePathOrder,
|
||||
Location,
|
||||
useCache,
|
||||
useExplorerLayoutStore,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useLibrarySubscription,
|
||||
useNodes,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import { Loader, Tooltip } from '@sd/ui';
|
||||
|
@ -52,8 +50,7 @@ export const Component = () => {
|
|||
keepPreviousData: true,
|
||||
suspense: true
|
||||
});
|
||||
useNodes(result.data?.nodes);
|
||||
const location = useCache(result.data?.item);
|
||||
const location = result.data;
|
||||
|
||||
// 'key' allows search state to be thrown out when entering a folder
|
||||
return <LocationExplorer key={path} location={location!} />;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
import { useBridgeQuery, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { useRouteTitle } from '~/hooks/useRouteTitle';
|
||||
import { hardwareModelToIcon } from '~/util/hardware';
|
||||
|
@ -20,8 +20,7 @@ export const Component = () => {
|
|||
const { t } = useLocale();
|
||||
|
||||
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items) ?? [];
|
||||
const locations = locationsQuery.data ?? [];
|
||||
|
||||
const { data: node } = useBridgeQuery(['nodeState']);
|
||||
|
||||
|
|
|
@ -8,15 +8,7 @@ import {
|
|||
Textbox
|
||||
} from '@phosphor-icons/react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
InOrNotIn,
|
||||
ObjectKind,
|
||||
SearchFilterArgs,
|
||||
TextMatch,
|
||||
useCache,
|
||||
useLibraryQuery,
|
||||
useNodes
|
||||
} from '@sd/client';
|
||||
import { InOrNotIn, ObjectKind, SearchFilterArgs, TextMatch, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Input } from '@sd/ui';
|
||||
import { Icon as SDIcon } from '~/components';
|
||||
|
||||
|
@ -437,8 +429,7 @@ export const filterRegistry = [
|
|||
},
|
||||
useOptions: () => {
|
||||
const query = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
useNodes(query.data?.nodes);
|
||||
const locations = useCache(query.data?.items);
|
||||
const locations = query.data;
|
||||
|
||||
return (locations ?? []).map((location) => ({
|
||||
name: location.name!,
|
||||
|
@ -473,8 +464,7 @@ export const filterRegistry = [
|
|||
},
|
||||
useOptions: () => {
|
||||
const query = useLibraryQuery(['tags.list']);
|
||||
useNodes(query.data?.nodes);
|
||||
const tags = useCache(query.data?.items);
|
||||
const tags = query.data;
|
||||
return (tags ?? []).map((tag) => ({
|
||||
name: tag.name!,
|
||||
value: tag.id,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { iconNames } from '@sd/assets/util';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { byteSize, useDiscoveredPeers, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { byteSize, useDiscoveredPeers, useLibraryQuery } from '@sd/client';
|
||||
import { Card } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
import { useCounter, useLocale } from '~/hooks';
|
||||
|
@ -16,8 +16,6 @@ export const Component = () => {
|
|||
const locations = useLibraryQuery(['locations.list'], {
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
useNodes(locations.data?.nodes);
|
||||
// const locations = useCache(result.data?.items);
|
||||
|
||||
const discoveredPeers = useDiscoveredPeers();
|
||||
const info = useMemo(() => {
|
||||
|
@ -38,8 +36,8 @@ export const Component = () => {
|
|||
}[] = [
|
||||
{
|
||||
icon: 'Folder',
|
||||
title: locations.data?.items.length === 1 ? 'Location' : 'Locations',
|
||||
titleCount: locations.data?.items.length ?? 0,
|
||||
title: locations.data?.length === 1 ? 'Location' : 'Locations',
|
||||
titleCount: locations.data?.length ?? 0,
|
||||
sub: 'indexed directories'
|
||||
},
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||
import { Suspense } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useCache, useLibraryMutation, useLibraryQuery, useNodes, useZodForm } from '@sd/client';
|
||||
import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client';
|
||||
import {
|
||||
Button,
|
||||
dialogManager,
|
||||
|
@ -57,8 +57,7 @@ const EditLocationForm = () => {
|
|||
const locationDataQuery = useLibraryQuery(['locations.getWithRules', locationId], {
|
||||
suspense: true
|
||||
});
|
||||
useNodes(locationDataQuery.data?.nodes);
|
||||
const locationData = useCache(locationDataQuery.data?.item);
|
||||
const locationData = locationDataQuery.data;
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
|
|
|
@ -4,10 +4,8 @@ import { useDebouncedCallback } from 'use-debounce';
|
|||
import {
|
||||
extractInfoRSPCError,
|
||||
UnionToTuple,
|
||||
useCache,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
usePlausibleEvent,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
|
@ -70,8 +68,7 @@ export const AddLocationDialog = ({
|
|||
const createLocation = useLibraryMutation('locations.create');
|
||||
const relinkLocation = useLibraryMutation('locations.relink');
|
||||
const listIndexerRulesQuery = useLibraryQuery(['locations.indexer_rules.list']);
|
||||
useNodes(listIndexerRulesQuery.data?.nodes);
|
||||
const listIndexerRules = useCache(listIndexerRulesQuery.data?.items);
|
||||
const listIndexerRules = listIndexerRulesQuery.data;
|
||||
const addLocationToLibrary = useLibraryMutation('locations.addLibrary');
|
||||
|
||||
// This is required because indexRules is undefined on first render
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Trash } from '@phosphor-icons/react';
|
|||
import clsx from 'clsx';
|
||||
import { MouseEventHandler, useState } from 'react';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
import { IndexerRule, useCache, useLibraryMutation, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { IndexerRule, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Divider, Label, toast } from '@sd/ui';
|
||||
import { InfoText } from '@sd/ui/src/forms';
|
||||
import { showAlertDialog } from '~/components';
|
||||
|
@ -33,8 +33,7 @@ export default function IndexerRuleEditor<T extends IndexerRuleIdFieldType>({
|
|||
...props
|
||||
}: IndexerRuleEditorProps<T>) {
|
||||
const listIndexerRules = useLibraryQuery(['locations.indexer_rules.list']);
|
||||
useNodes(listIndexerRules.data?.nodes);
|
||||
const indexRules = useCache(listIndexerRules.data?.items);
|
||||
const indexRules = listIndexerRules.data;
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [selectedRule, setSelectedRule] = useState<IndexerRule | undefined>(undefined);
|
||||
const [toggleNewRule, setToggleNewRule] = useState(false);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { SearchInput } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
||||
|
@ -10,8 +10,7 @@ import ListItem from './ListItem';
|
|||
|
||||
export const Component = () => {
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const locations = locationsQuery.data;
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [debouncedSearch] = useDebounce(search, 200);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Card, dialogManager } from '@sd/ui';
|
||||
import { Heading } from '~/app/$libraryId/settings/Layout';
|
||||
import { TagsSettingsParamsSchema } from '~/app/route-schemas';
|
||||
|
@ -11,8 +11,7 @@ import EditForm from './EditForm';
|
|||
|
||||
export const Component = () => {
|
||||
const result = useLibraryQuery(['tags.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const tags = useCache(result.data?.items);
|
||||
const tags = result.data;
|
||||
|
||||
const { id: locationId } = useZodRouteParams(TagsSettingsParamsSchema);
|
||||
const tagSelectedParam = tags?.find((tag) => tag.id === locationId);
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
insertLibrary,
|
||||
useBridgeMutation,
|
||||
useNormalisedCache,
|
||||
usePlausibleEvent,
|
||||
useZodForm
|
||||
} from '@sd/client';
|
||||
import { insertLibrary, useBridgeMutation, usePlausibleEvent, useZodForm } from '@sd/client';
|
||||
import { Dialog, InputField, useDialog, UseDialogProps, z } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
@ -32,16 +26,13 @@ export default (props: UseDialogProps) => {
|
|||
const createLibrary = useBridgeMutation('library.create');
|
||||
|
||||
const form = useZodForm({ schema });
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
try {
|
||||
const libraryRaw = await createLibrary.mutateAsync({
|
||||
const library = await createLibrary.mutateAsync({
|
||||
name: data.name,
|
||||
default_locations: null
|
||||
});
|
||||
cache.withNodes(libraryRaw.nodes);
|
||||
const library = cache.withCache(libraryRaw.item);
|
||||
|
||||
insertLibrary(queryClient, library);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useBridgeQuery, useCache, useLibraryContext, useNodes } from '@sd/client';
|
||||
import { useBridgeQuery, useLibraryContext } from '@sd/client';
|
||||
import { Button, dialogManager } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
||||
|
@ -8,8 +8,7 @@ import ListItem from './ListItem';
|
|||
|
||||
export const Component = () => {
|
||||
const librariesQuery = useBridgeQuery(['library.list']);
|
||||
useNodes(librariesQuery.data?.nodes);
|
||||
const libraries = useCache(librariesQuery.data?.items);
|
||||
const libraries = librariesQuery.data;
|
||||
|
||||
const { library } = useLibraryContext();
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { ObjectOrder, Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { ObjectOrder, Tag, useLibraryQuery } from '@sd/client';
|
||||
import { LocationIdParamsSchema } from '~/app/route-schemas';
|
||||
import { Icon } from '~/components';
|
||||
import { useLocale, useRouteTitle, useZodRouteParams } from '~/hooks';
|
||||
|
@ -20,18 +20,17 @@ import { TopBarPortal } from '../TopBar/Portal';
|
|||
export function Component() {
|
||||
const { id: tagId } = useZodRouteParams(LocationIdParamsSchema);
|
||||
const result = useLibraryQuery(['tags.get', tagId], { suspense: true });
|
||||
useNodes(result.data?.nodes);
|
||||
const tag = useCache(result.data?.item);
|
||||
const tag = result.data;
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
useRouteTitle(tag!.name ?? 'Tag');
|
||||
|
||||
const { explorerSettings, preferences } = useTagExplorerSettings(tag);
|
||||
const { explorerSettings, preferences } = useTagExplorerSettings(tag!);
|
||||
|
||||
const search = useSearchFromSearchParams();
|
||||
|
||||
const defaultFilters = useMemo(() => [{ object: { tags: { in: [tag.id] } } }], [tag.id]);
|
||||
const defaultFilters = useMemo(() => [{ object: { tags: { in: [tag!.id] } } }], [tag!.id]);
|
||||
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
|
|
|
@ -11,22 +11,17 @@ import {
|
|||
type RouteObject
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
CacheProvider,
|
||||
ClientContextProvider,
|
||||
context,
|
||||
context2,
|
||||
createCache,
|
||||
currentLibraryCache,
|
||||
getCachedLibraries,
|
||||
LibraryContextProvider,
|
||||
nonLibraryClient,
|
||||
NormalisedCache,
|
||||
Procedures,
|
||||
useBridgeQuery,
|
||||
useCache,
|
||||
useCachedLibraries,
|
||||
useFeatureFlag,
|
||||
useNodes,
|
||||
WithSolid,
|
||||
type LibraryProceduresDef,
|
||||
type NonLibraryProceduresDef
|
||||
|
@ -60,7 +55,7 @@ function P2PErrorToast() {
|
|||
return null;
|
||||
}
|
||||
|
||||
export const createRoutes = (platform: Platform, cache: NormalisedCache) =>
|
||||
export const createRoutes = (platform: Platform) =>
|
||||
[
|
||||
{
|
||||
Component: () => {
|
||||
|
@ -103,9 +98,9 @@ export const createRoutes = (platform: Platform, cache: NormalisedCache) =>
|
|||
return <Navigate to={`${libraryId}`} replace />;
|
||||
},
|
||||
loader: async () => {
|
||||
const libraries = await getCachedLibraries(cache, nonLibraryClient);
|
||||
const libraries = await getCachedLibraries(nonLibraryClient);
|
||||
|
||||
const currentLibrary = libraries.find(
|
||||
const currentLibrary = (libraries || []).find(
|
||||
(l) => l.uuid === currentLibraryCache.id
|
||||
);
|
||||
|
||||
|
@ -135,8 +130,7 @@ export const createRoutes = (platform: Platform, cache: NormalisedCache) =>
|
|||
Component: () => {
|
||||
const params = useZodRouteParams(LibraryIdParamsSchema);
|
||||
const result = useBridgeQuery(['library.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const libraries = useCache(result.data?.items);
|
||||
const libraries = result.data;
|
||||
|
||||
const library = libraries?.find((l) => l.uuid === params.libraryId);
|
||||
|
||||
|
@ -176,7 +170,7 @@ export const createRoutes = (platform: Platform, cache: NormalisedCache) =>
|
|||
path: ':libraryId',
|
||||
lazy: () => import('./$libraryId/Layout'),
|
||||
loader: async ({ params: { libraryId } }) => {
|
||||
const libraries = await getCachedLibraries(cache, nonLibraryClient);
|
||||
const libraries = await getCachedLibraries(nonLibraryClient);
|
||||
const library = libraries.find((l) => l.uuid === libraryId);
|
||||
|
||||
if (!library) {
|
||||
|
@ -204,12 +198,7 @@ function RemoteLayout() {
|
|||
// TODO: The caches should instead be prefixed by the remote node ID, instead of completely being recreated but that's too hard to do right now.
|
||||
const [rspcClient, setRspcClient] =
|
||||
useState<
|
||||
[
|
||||
AlphaClient<NonLibraryProceduresDef>,
|
||||
AlphaClient<LibraryProceduresDef>,
|
||||
QueryClient,
|
||||
NormalisedCache
|
||||
]
|
||||
[AlphaClient<NonLibraryProceduresDef>, AlphaClient<LibraryProceduresDef>, QueryClient]
|
||||
>();
|
||||
useEffect(() => {
|
||||
const endpoint = platform.getRemoteRspcEndpoint(params.node);
|
||||
|
@ -233,8 +222,7 @@ function RemoteLayout() {
|
|||
return [keyAndInput[0], { library_id: libraryId, arg: keyAndInput[1] ?? null }];
|
||||
}
|
||||
});
|
||||
const cache = createCache();
|
||||
setRspcClient([client, libraryClient, new QueryClient(), cache]);
|
||||
setRspcClient([client, libraryClient, new QueryClient()]);
|
||||
|
||||
return () => {
|
||||
// TODO: We *really* need to cleanup `client` so we aren't leaking all the resources.
|
||||
|
@ -273,25 +261,23 @@ function RemoteLayout() {
|
|||
{/* TODO: Maybe library context too? */}
|
||||
{rspcClient && (
|
||||
<QueryClientProvider client={rspcClient[2]}>
|
||||
<CacheProvider cache={rspcClient[3]}>
|
||||
<context.Provider
|
||||
<context.Provider
|
||||
value={{
|
||||
// @ts-expect-error
|
||||
client: rspcClient[0],
|
||||
queryClient: rspcClient[2]
|
||||
}}
|
||||
>
|
||||
<context2.Provider
|
||||
value={{
|
||||
// @ts-expect-error
|
||||
client: rspcClient[0],
|
||||
client: rspcClient[1],
|
||||
queryClient: rspcClient[2]
|
||||
}}
|
||||
>
|
||||
<context2.Provider
|
||||
value={{
|
||||
// @ts-expect-error
|
||||
client: rspcClient[1],
|
||||
queryClient: rspcClient[2]
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</context2.Provider>
|
||||
</context.Provider>
|
||||
</CacheProvider>
|
||||
<Outlet />
|
||||
</context2.Provider>
|
||||
</context.Provider>
|
||||
</QueryClientProvider>
|
||||
)}
|
||||
</PlatformProvider>
|
||||
|
@ -301,8 +287,7 @@ function RemoteLayout() {
|
|||
function BrowsePage() {
|
||||
const navigate = useNavigate();
|
||||
const result = useBridgeQuery(['library.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const libraries = useCache(result.data?.items);
|
||||
const libraries = result.data;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
useBridgeMutation,
|
||||
useCachedLibraries,
|
||||
useMultiZodForm,
|
||||
useNormalisedCache,
|
||||
useOnboardingStore,
|
||||
usePlausibleEvent
|
||||
} from '@sd/client';
|
||||
|
@ -98,7 +97,6 @@ const useFormState = () => {
|
|||
}
|
||||
|
||||
const createLibrary = useBridgeMutation('library.create');
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
const submit = handleSubmit(
|
||||
async (data) => {
|
||||
|
@ -110,15 +108,13 @@ const useFormState = () => {
|
|||
|
||||
try {
|
||||
// show creation screen for a bit for smoothness
|
||||
const [libraryRaw] = await Promise.all([
|
||||
const [library] = await Promise.all([
|
||||
createLibrary.mutateAsync({
|
||||
name: data['new-library'].name,
|
||||
default_locations: data.locations.locations
|
||||
}),
|
||||
new Promise((res) => setTimeout(res, 500))
|
||||
]);
|
||||
cache.withNodes(libraryRaw.nodes);
|
||||
const library = cache.withCache(libraryRaw.item);
|
||||
insertLibrary(queryClient, library);
|
||||
|
||||
platform.refreshMenuBar && platform.refreshMenuBar();
|
||||
|
|
|
@ -61,7 +61,7 @@ function CloudLibraries() {
|
|||
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||
// The invalidation system beat us to it
|
||||
if (libraries.find((l: any) => l.uuid === library.uuid))
|
||||
if ((libraries || []).find((l: any) => l.uuid === library.uuid))
|
||||
return libraries;
|
||||
|
||||
return [...(libraries || []), library];
|
||||
|
|
|
@ -1,296 +0,0 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore
|
||||
} from 'react';
|
||||
import { proxy, snapshot, subscribe } from 'valtio';
|
||||
|
||||
import { type CacheNode } from './core';
|
||||
import { getPermits } from './rspc-cursed';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__REDUX_DEVTOOLS_EXTENSION__: any;
|
||||
}
|
||||
}
|
||||
|
||||
type Store = ReturnType<typeof defaultStore>;
|
||||
type Context = ReturnType<typeof createCache>;
|
||||
export type NormalisedCache = ReturnType<typeof createCache>;
|
||||
|
||||
const defaultStore = () => ({
|
||||
nodes: {} as Record<string, Record<string, unknown>>
|
||||
});
|
||||
|
||||
const Context = createContext<Context>(undefined!);
|
||||
|
||||
export function createCache() {
|
||||
const cache = proxy(defaultStore());
|
||||
return {
|
||||
cache,
|
||||
withNodes(data: CacheNode[] | undefined, suffix?: string) {
|
||||
updateNodes(cache, data, suffix);
|
||||
},
|
||||
withCache<T>(data: T | undefined, suffix?: string): UseCacheResult<T> {
|
||||
return restore(cache, new Map(), data, suffix) as any;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function CacheProvider({ cache, children }: PropsWithChildren<{ cache: NormalisedCache }>) {
|
||||
useEffect(() => {
|
||||
if ('__REDUX_DEVTOOLS_EXTENSION__' in window === false) return;
|
||||
|
||||
const devtools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({});
|
||||
|
||||
const unsub = devtools.subscribe((_message: any) => {
|
||||
// console.log(message);
|
||||
});
|
||||
|
||||
devtools.init();
|
||||
subscribe(cache.cache, () => devtools.send('change', snapshot(cache.cache)));
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const permits = getPermits();
|
||||
if (permits !== 0) {
|
||||
console.warn('Not safe to cleanup cache. ${permits} permits currently held.');
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredKeys = new StableSet<[string, string]>();
|
||||
for (const query of queryClient.getQueryCache().getAll()) {
|
||||
if (query.state.data) scanDataForKeys(cache.cache, requiredKeys, query.state.data);
|
||||
}
|
||||
|
||||
const existingKeys = new StableSet<[string, string]>();
|
||||
Object.entries(cache.cache.nodes).map(([type, value]) => {
|
||||
Object.keys(value).map((id) => existingKeys.add([type, id]));
|
||||
});
|
||||
|
||||
for (const [type, id] of existingKeys.entries()) {
|
||||
// If key is not required. Eg. not in any query within the React Query cache.
|
||||
if (!requiredKeys.has([type, id])) {
|
||||
// Yeet the imposter
|
||||
delete cache.cache.nodes?.[type]?.[id];
|
||||
}
|
||||
}
|
||||
}, 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [cache.cache, queryClient]);
|
||||
|
||||
return <Context.Provider value={cache}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
export function useCacheContext() {
|
||||
const context = useContext(Context);
|
||||
if (!context) throw new Error('Missing `CacheContext` provider!');
|
||||
return context;
|
||||
}
|
||||
|
||||
function scanDataForKeys(cache: Store, keys: StableSet<[string, string]>, item: unknown) {
|
||||
if (item === undefined || item === null) return;
|
||||
if (Array.isArray(item)) {
|
||||
for (const v of item) {
|
||||
scanDataForKeys(cache, keys, v);
|
||||
}
|
||||
} else if (typeof item === 'object') {
|
||||
if ('__type' in item && '__id' in item) {
|
||||
if (typeof item.__type !== 'string') throw new Error('Invalid `__type`');
|
||||
if (typeof item.__id !== 'string') throw new Error('Invalid `__id`');
|
||||
keys.add([item.__type, item.__id]);
|
||||
const result = cache.nodes?.[item.__type]?.[item.__id];
|
||||
if (result) scanDataForKeys(cache, keys, result);
|
||||
}
|
||||
|
||||
for (const [_k, value] of Object.entries(item)) {
|
||||
scanDataForKeys(cache, keys, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function restore(
|
||||
cache: Store,
|
||||
subscribed: Map<string, Set<unknown>>,
|
||||
item: unknown,
|
||||
suffix?: string
|
||||
): unknown {
|
||||
if (item === undefined || item === null) {
|
||||
return item;
|
||||
} else if (Array.isArray(item)) {
|
||||
return item.map((v) => restore(cache, subscribed, v));
|
||||
} else if (typeof item === 'object') {
|
||||
if ('__type' in item && '__id' in item) {
|
||||
if (typeof item.__type !== 'string') throw new Error('Invalid `__type`');
|
||||
if (typeof item.__id !== 'string') throw new Error('Invalid `__id`');
|
||||
const ty = suffix ? `${suffix}:${item.__type}` : item.__type;
|
||||
|
||||
const result = cache.nodes?.[ty]?.[item.__id];
|
||||
if (!result) throw new Error(`Missing node for id '${item.__id}' of type '${ty}'`);
|
||||
|
||||
const v = subscribed.get(ty);
|
||||
if (v) {
|
||||
v.add(item.__id);
|
||||
} else {
|
||||
subscribed.set(ty, new Set([item.__id]));
|
||||
}
|
||||
|
||||
// We call restore again for arrays and objects to deal with nested relations.
|
||||
return Object.fromEntries(
|
||||
Object.entries(result).map(([key, value]) => [
|
||||
key,
|
||||
restore(cache, subscribed, value)
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(item).map(([key, value]) => [key, restore(cache, subscribed, value)])
|
||||
);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
export function useNodes(data: CacheNode[] | undefined) {
|
||||
const cache = useCacheContext();
|
||||
|
||||
// `useMemo` instead of `useEffect` here is cursed but it needs to run before the `useMemo` in the `useCache` hook.
|
||||
useMemo(() => {
|
||||
updateNodes(cache.cache, data);
|
||||
}, [cache, data]);
|
||||
}
|
||||
|
||||
// Methods to interact with the cache outside of the React lifecycle.
|
||||
export function useNormalisedCache() {
|
||||
const cache = useCacheContext();
|
||||
|
||||
return {
|
||||
'#cache': cache.cache,
|
||||
'withNodes': cache.withNodes,
|
||||
'withCache': cache.withCache
|
||||
};
|
||||
}
|
||||
|
||||
function updateNodes(cache: Store, data: CacheNode[] | undefined, suffix?: string) {
|
||||
if (!data) return;
|
||||
|
||||
for (const item of data) {
|
||||
if (!('__type' in item && '__id' in item)) throw new Error('Missing `__type` or `__id`');
|
||||
if (typeof item.__type !== 'string') throw new Error('Invalid `__type`');
|
||||
if (typeof item.__id !== 'string') throw new Error('Invalid `__id`');
|
||||
const ty = suffix ? `${suffix}:${item.__type}` : item.__type;
|
||||
|
||||
const copy = { ...item } as any;
|
||||
delete copy.__type;
|
||||
delete copy.__id;
|
||||
|
||||
const original = cache.nodes?.[ty]?.[item.__id];
|
||||
specialMerge(copy, original);
|
||||
|
||||
if (!cache.nodes[ty]) cache.nodes[ty] = {};
|
||||
// TODO: This should be a deepmerge but that would break stuff like `size_in_bytes` or `inode` as the arrays are joined.
|
||||
cache.nodes[ty]![item.__id] = copy;
|
||||
}
|
||||
}
|
||||
|
||||
// When using PCR's data structure if you don't fetch a relation `null` is returned.
|
||||
// If two queries return a single entity but one fetches relations and the other doesn't that null might "win" over the actual data.
|
||||
// Once it "wins" the normalised cache is updated causing all `useCache`'s to rerun.
|
||||
//
|
||||
// The `useCache` hook derives the type from the specific React Query operation.
|
||||
// Due to this the result of a `useCache` might end up as `null` even when TS says it's `T` causing crashes due to no-null checks.
|
||||
//
|
||||
// So this merge function causes the `null` to be replaced with the original value.
|
||||
function specialMerge(copy: Record<any, any>, original: unknown) {
|
||||
if (
|
||||
original &&
|
||||
typeof original === 'object' &&
|
||||
typeof copy === 'object' &&
|
||||
!Array.isArray(original) &&
|
||||
!Array.isArray(copy)
|
||||
) {
|
||||
for (const [property, value] of Object.entries(original)) {
|
||||
copy[property] = copy[property] || value;
|
||||
|
||||
if (typeof copy[property] === 'object' && !Array.isArray(copy[property]))
|
||||
specialMerge(copy[property], value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type UseCacheResult<T> = T extends (infer A)[]
|
||||
? UseCacheResult<A>[]
|
||||
: T extends object
|
||||
? T extends { '__type': any; '__id': string; '#type': infer U }
|
||||
? UseCacheResult<U>
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> }
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> };
|
||||
|
||||
export function useCache<T>(data: T | undefined) {
|
||||
const cache = useCacheContext();
|
||||
const subscribed = useRef(new Map<string, Set<unknown>>()).current;
|
||||
const [i, setI] = useState(0); // TODO: Remove this
|
||||
|
||||
const state = useMemo(
|
||||
() => restore(cache.cache, subscribed, data) as UseCacheResult<T>,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[cache, data, i]
|
||||
);
|
||||
|
||||
return useSyncExternalStore(
|
||||
(onStoreChange) => {
|
||||
return subscribe(cache.cache, (ops) => {
|
||||
for (const [_, key] of ops) {
|
||||
const key_type = key[1] as string;
|
||||
const key_id = key[2] as string;
|
||||
|
||||
const v = subscribed.get(key_type);
|
||||
if (v && v.has(key_id)) {
|
||||
setI((i) => i + 1);
|
||||
onStoreChange();
|
||||
|
||||
break; // We only need to trigger re-render once so we can break
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
() => state
|
||||
);
|
||||
}
|
||||
|
||||
class StableSet<T> {
|
||||
set = new Set<string>();
|
||||
|
||||
get size() {
|
||||
return this.set.size;
|
||||
}
|
||||
|
||||
add(value: T) {
|
||||
this.set.add(JSON.stringify(value));
|
||||
}
|
||||
|
||||
has(value: T) {
|
||||
return this.set.has(JSON.stringify(value));
|
||||
}
|
||||
|
||||
*entries() {
|
||||
for (const v of this.set) {
|
||||
yield JSON.parse(v);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ export type Procedures = {
|
|||
{ key: "cloud.library.list", input: never, result: CloudLibrary[] } |
|
||||
{ key: "cloud.locations.list", input: never, result: CloudLocation[] } |
|
||||
{ key: "ephemeralFiles.getMediaData", input: string, result: ({ type: "Image" } & ImageMetadata) | ({ type: "Video" } & VideoMetadata) | ({ type: "Audio" } & AudioMetadata) | null } |
|
||||
{ key: "files.get", input: LibraryArgs<number>, result: { item: Reference<ObjectWithFilePaths2>; nodes: CacheNode[] } | null } |
|
||||
{ key: "files.get", input: LibraryArgs<number>, result: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: ({ id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; hidden: boolean | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; media_data: { resolution: number[] | null; media_date: number[] | null; media_location: number[] | null; camera_data: number[] | null; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null } | null } | null })[] } | null } |
|
||||
{ key: "files.getConvertibleImageExtensions", input: never, result: string[] } |
|
||||
{ key: "files.getMediaData", input: LibraryArgs<number>, result: MediaMetadata } |
|
||||
{ key: "files.getPath", input: LibraryArgs<number>, result: string | null } |
|
||||
|
@ -25,14 +25,14 @@ export type Procedures = {
|
|||
{ key: "labels.list", input: LibraryArgs<null>, result: Label[] } |
|
||||
{ key: "labels.listWithThumbnails", input: LibraryArgs<string>, result: ExplorerItem[] } |
|
||||
{ key: "library.kindStatistics", input: LibraryArgs<null>, result: KindStatistics } |
|
||||
{ key: "library.list", input: never, result: NormalisedResults<LibraryConfigWrapped> } |
|
||||
{ key: "library.list", input: never, result: LibraryConfigWrapped[] } |
|
||||
{ key: "library.statistics", input: LibraryArgs<null>, result: StatisticsResponse } |
|
||||
{ key: "locations.get", input: LibraryArgs<number>, result: { item: Reference<Location>; nodes: CacheNode[] } | null } |
|
||||
{ key: "locations.getWithRules", input: LibraryArgs<number>, result: { item: Reference<LocationWithIndexerRule>; nodes: CacheNode[] } | null } |
|
||||
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: NormalisedResult<IndexerRule> } |
|
||||
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: NormalisedResults<IndexerRule> } |
|
||||
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: NormalisedResults<IndexerRule> } |
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: NormalisedResults<Location> } |
|
||||
{ key: "locations.get", input: LibraryArgs<number>, result: { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; scan_state: number; instance_id: number | null } | null } |
|
||||
{ key: "locations.getWithRules", input: LibraryArgs<number>, result: { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null; indexer_rules: IndexerRule[] } | null } |
|
||||
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
|
||||
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: IndexerRule[] } |
|
||||
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: IndexerRule[] } |
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: Location[] } |
|
||||
{ key: "locations.systemLocations", input: never, result: SystemLocations } |
|
||||
{ key: "models.image_detection.list", input: never, result: string[] } |
|
||||
{ key: "nodeState", input: never, result: NodeState } |
|
||||
|
@ -51,11 +51,11 @@ export type Procedures = {
|
|||
{ key: "search.saved.list", input: LibraryArgs<null>, result: SavedSearch[] } |
|
||||
{ key: "sync.enabled", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "sync.messages", input: LibraryArgs<null>, result: CRDTOperation[] } |
|
||||
{ key: "tags.get", input: LibraryArgs<number>, result: { item: Reference<Tag>; nodes: CacheNode[] } | null } |
|
||||
{ key: "tags.getForObject", input: LibraryArgs<number>, result: NormalisedResults<Tag> } |
|
||||
{ key: "tags.get", input: LibraryArgs<number>, result: { id: number; pub_id: number[]; name: string | null; color: string | null; is_hidden: boolean | null; date_created: string | null; date_modified: string | null } | null } |
|
||||
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Tag[] } |
|
||||
{ key: "tags.getWithObjects", input: LibraryArgs<number[]>, result: { [key in number]: ({ date_created: string | null; object: { id: number } })[] } } |
|
||||
{ key: "tags.list", input: LibraryArgs<null>, result: NormalisedResults<Tag> } |
|
||||
{ key: "volumes.list", input: never, result: NormalisedResults<Volume> },
|
||||
{ key: "tags.list", input: LibraryArgs<null>, result: Tag[] } |
|
||||
{ key: "volumes.list", input: never, result: Volume[] },
|
||||
mutations:
|
||||
{ key: "api.sendFeedback", input: Feedback, result: null } |
|
||||
{ key: "auth.logout", input: never, result: null } |
|
||||
|
@ -100,7 +100,7 @@ export type Procedures = {
|
|||
{ key: "jobs.pause", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "jobs.resume", input: LibraryArgs<string>, result: null } |
|
||||
{ key: "labels.delete", input: LibraryArgs<number>, result: null } |
|
||||
{ key: "library.create", input: CreateLibraryArgs, result: NormalisedResult<LibraryConfigWrapped> } |
|
||||
{ key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } |
|
||||
{ key: "library.delete", input: string, result: null } |
|
||||
{ key: "library.edit", input: EditLibraryArgs, result: null } |
|
||||
{ key: "library.startActor", input: LibraryArgs<string>, result: null } |
|
||||
|
@ -166,8 +166,6 @@ export type CRDTOperation = { instance: string; timestamp: number; model: number
|
|||
|
||||
export type CRDTOperationData = { c: { [key in string]: JsonValue } } | { u: { field: string; value: JsonValue } } | "d"
|
||||
|
||||
export type CacheNode = { __type: string; __id: string; "#node": any }
|
||||
|
||||
export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null }
|
||||
|
||||
export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_ipv4_enabled: boolean | null; p2p_ipv6_enabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; p2p_remote_access: boolean | null; image_labeler_version: string | null }
|
||||
|
@ -242,7 +240,7 @@ export type EphemeralPathOrder = { field: "name"; value: SortOrder } | { field:
|
|||
|
||||
export type EphemeralPathSearchArgs = { path: string; withHiddenFiles: boolean; order?: EphemeralPathOrder | null }
|
||||
|
||||
export type EphemeralPathsResultItem = { entries: Reference<ExplorerItem>[]; errors: Error[]; nodes: CacheNode[] }
|
||||
export type EphemeralPathsResultItem = { entries: ExplorerItem[]; errors: Error[] }
|
||||
|
||||
export type EphemeralRenameFileArgs = { kind: EphemeralRenameKind }
|
||||
|
||||
|
@ -444,8 +442,6 @@ export type LocationSettings = { explorer: ExplorerSettings<FilePathOrder> }
|
|||
*/
|
||||
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[]; path: string | null }
|
||||
|
||||
export type LocationWithIndexerRule = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null; indexer_rules: Reference<IndexerRule>[] }
|
||||
|
||||
export type MaybeUndefined<T> = null | T
|
||||
|
||||
export type MediaDataOrder = { field: "epochTime"; value: SortOrder }
|
||||
|
@ -476,20 +472,6 @@ name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; features: BackendFea
|
|||
|
||||
export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[]; hidden: boolean }
|
||||
|
||||
/**
|
||||
* A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
*
|
||||
* You don't need to use this, it's just a shortcut to avoid having to write out the full type every time.
|
||||
*/
|
||||
export type NormalisedResult<T> = { item: Reference<T>; nodes: CacheNode[] }
|
||||
|
||||
/**
|
||||
* A type that can be used to return a group of `Reference<T>` and `CacheNode`'s
|
||||
*
|
||||
* You don't need to use this, it's just a shortcut to avoid having to write out the full type every time.
|
||||
*/
|
||||
export type NormalisedResults<T> = { items: Reference<T>[]; nodes: CacheNode[] }
|
||||
|
||||
/**
|
||||
* Represents a single notification.
|
||||
*/
|
||||
|
@ -521,8 +503,6 @@ export type ObjectValidatorArgs = { id: number; path: string }
|
|||
|
||||
export type ObjectWithFilePaths = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: ({ id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; hidden: boolean | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; media_data: { resolution: number[] | null; media_date: number[] | null; media_location: number[] | null; camera_data: number[] | null; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null } | null } | null })[] }
|
||||
|
||||
export type ObjectWithFilePaths2 = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: Reference<FilePath>[] }
|
||||
|
||||
export type OldFileCopierJobInit = { source_location_id: number; target_location_id: number; sources_file_path_ids: number[]; target_location_relative_directory_path: string }
|
||||
|
||||
export type OldFileCutterJobInit = { source_location_id: number; target_location_id: number; sources_file_path_ids: number[]; target_location_relative_directory_path: string }
|
||||
|
@ -553,16 +533,6 @@ export type Port = { type: "random" } | { type: "discrete"; value: number }
|
|||
|
||||
export type Range<T> = { from: T } | { to: T }
|
||||
|
||||
/**
|
||||
* A reference to a `CacheNode`.
|
||||
*
|
||||
* This does not contain the actual data, but instead a reference to it.
|
||||
* This allows the CacheNode's to be switched out and the query recomputed without any backend communication.
|
||||
*
|
||||
* If you use a `Reference` in a query, you *must* ensure the corresponding `CacheNode` is also in the query.
|
||||
*/
|
||||
export type Reference<T> = { __type: string; __id: string; "#type": T }
|
||||
|
||||
export type RemoteIdentity = string
|
||||
|
||||
export type RenameFileArgs = { location_id: number; kind: RenameKind }
|
||||
|
@ -583,7 +553,7 @@ export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChil
|
|||
|
||||
export type SavedSearch = { id: number; pub_id: number[]; target: string | null; search: string | null; filters: string | null; name: string | null; icon: string | null; description: string | null; date_created: string | null; date_modified: string | null }
|
||||
|
||||
export type SearchData<T> = { cursor: number[] | null; items: Reference<T>[]; nodes: CacheNode[] }
|
||||
export type SearchData<T> = { cursor: number[] | null; items: T[] }
|
||||
|
||||
export type SearchFilterArgs = { filePath: FilePathFilterArgs } | { object: ObjectFilterArgs }
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { UseInfiniteQueryResult, UseQueryResult } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useCache } from '../cache';
|
||||
import { SearchData } from '../core';
|
||||
|
||||
export function useExplorerQuery<Q>(
|
||||
|
@ -16,7 +15,7 @@ export function useExplorerQuery<Q>(
|
|||
}
|
||||
}, [query.hasNextPage, query.isFetchingNextPage, query.fetchNextPage]);
|
||||
|
||||
return { query, items: useCache(items), loadMore, count: count.data };
|
||||
return { query, items, loadMore, count: count.data };
|
||||
}
|
||||
|
||||
export type UseExplorerQuery<Q> = ReturnType<typeof useExplorerQuery<Q>>;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodes } from '../cache';
|
||||
import { ExplorerItem, ObjectCursor, ObjectOrder, ObjectSearchArgs } from '../core';
|
||||
import { useLibraryContext } from '../hooks';
|
||||
import { useRspcLibraryContext } from '../rspc';
|
||||
|
@ -57,17 +56,10 @@ export function useObjectsInfiniteQuery({
|
|||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.items.length < arg.take) return undefined;
|
||||
else return lastPage.nodes[arg.take - 1];
|
||||
else return lastPage.items[arg.take - 1];
|
||||
},
|
||||
...args
|
||||
});
|
||||
|
||||
const nodes = useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.nodes) ?? [],
|
||||
[query.data?.pages]
|
||||
);
|
||||
|
||||
useNodes(nodes);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodes, useNormalisedCache } from '../cache';
|
||||
import { ObjectOrder, ObjectSearchArgs } from '../core';
|
||||
import { useLibraryContext } from '../hooks';
|
||||
import { useRspcLibraryContext } from '../rspc';
|
||||
|
@ -14,7 +13,6 @@ export function useObjectsOffsetInfiniteQuery({
|
|||
}: UseExplorerInfiniteQueryArgs<ObjectSearchArgs, ObjectOrder>) {
|
||||
const { library } = useLibraryContext();
|
||||
const ctx = useRspcLibraryContext();
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
if (order) {
|
||||
arg.orderAndPagination = { orderOnly: order };
|
||||
|
@ -39,22 +37,14 @@ export function useObjectsOffsetInfiniteQuery({
|
|||
arg.orderAndPagination = orderAndPagination;
|
||||
|
||||
const result = await ctx.client.query(['search.objects', arg]);
|
||||
cache.withNodes(result.nodes);
|
||||
|
||||
return { ...result, offset: pageParam, arg };
|
||||
},
|
||||
getNextPageParam: ({ nodes, offset, arg }) => {
|
||||
if (nodes.length >= arg.take) return (offset ?? 0) + 1;
|
||||
getNextPageParam: ({ items, offset, arg }) => {
|
||||
if (items.length >= arg.take) return (offset ?? 0) + 1;
|
||||
},
|
||||
...args
|
||||
});
|
||||
|
||||
const nodes = useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.nodes) ?? [],
|
||||
[query.data?.pages]
|
||||
);
|
||||
|
||||
useNodes(nodes);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodes, useNormalisedCache } from '../cache';
|
||||
import {
|
||||
ExplorerItem,
|
||||
FilePathCursorVariant,
|
||||
|
@ -21,7 +19,6 @@ export function usePathsInfiniteQuery({
|
|||
}: UseExplorerInfiniteQueryArgs<FilePathSearchArgs, FilePathOrder>) {
|
||||
const { library } = useLibraryContext();
|
||||
const ctx = useRspcLibraryContext();
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
if (order) {
|
||||
arg.orderAndPagination = { orderOnly: order };
|
||||
|
@ -122,24 +119,16 @@ export function usePathsInfiniteQuery({
|
|||
arg.orderAndPagination = orderAndPagination;
|
||||
|
||||
const result = await ctx.client.query(['search.paths', arg]);
|
||||
cache.withNodes(result.nodes);
|
||||
return result;
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (arg.take === null || arg.take === undefined) return undefined;
|
||||
if (lastPage.items.length < arg.take) return undefined;
|
||||
else return lastPage.nodes[arg.take - 1];
|
||||
else return lastPage.items[arg.take - 1];
|
||||
},
|
||||
onSuccess,
|
||||
...args
|
||||
});
|
||||
|
||||
const nodes = useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.nodes) ?? [],
|
||||
[query.data?.pages]
|
||||
);
|
||||
|
||||
useNodes(nodes);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodes, useNormalisedCache } from '../cache';
|
||||
import { FilePathOrder, FilePathSearchArgs } from '../core';
|
||||
import { useLibraryContext } from '../hooks';
|
||||
import { useRspcLibraryContext } from '../rspc';
|
||||
|
@ -17,7 +15,6 @@ export function usePathsOffsetInfiniteQuery({
|
|||
|
||||
const { library } = useLibraryContext();
|
||||
const ctx = useRspcLibraryContext();
|
||||
const cache = useNormalisedCache();
|
||||
|
||||
if (order) {
|
||||
arg.orderAndPagination = { orderOnly: order };
|
||||
|
@ -49,23 +46,15 @@ export function usePathsOffsetInfiniteQuery({
|
|||
arg.orderAndPagination = orderAndPagination;
|
||||
|
||||
const result = await ctx.client.query(['search.paths', arg]);
|
||||
cache.withNodes(result.nodes);
|
||||
|
||||
return { ...result, offset: pageParam, arg };
|
||||
},
|
||||
getNextPageParam: ({ nodes, offset, arg }) => {
|
||||
if (nodes.length >= arg.take) return (offset ?? 0) + 1;
|
||||
getNextPageParam: ({ items, offset, arg }) => {
|
||||
if (items.length >= arg.take) return (offset ?? 0) + 1;
|
||||
},
|
||||
onSuccess,
|
||||
...args
|
||||
});
|
||||
|
||||
const nodes = useMemo(
|
||||
() => query.data?.pages.flatMap((page) => page.nodes) ?? [],
|
||||
[query.data?.pages]
|
||||
);
|
||||
|
||||
useNodes(nodes);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { AlphaClient } from '@oscartbeaumont-sd/rspc-client/v2';
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react';
|
||||
|
||||
import { NormalisedCache, useCache, useNodes } from '../cache';
|
||||
import { LibraryConfigWrapped, Procedures } from '../core';
|
||||
import { valtioPersist } from '../lib';
|
||||
import { useBridgeQuery } from '../rspc';
|
||||
|
||||
// The name of the localStorage key for caching library data
|
||||
const libraryCacheLocalStorageKey = 'sd-library-list2'; // `2` is because the format of this underwent a breaking change when introducing normalised caching
|
||||
const libraryCacheLocalStorageKey = 'sd-library-list3'; // number is because the format of this underwent breaking changes
|
||||
|
||||
export const useCachedLibraries = () => {
|
||||
const result = useBridgeQuery(['library.list'], {
|
||||
|
@ -27,36 +26,27 @@ export const useCachedLibraries = () => {
|
|||
return undefined;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.items.length > 0 || data.nodes.length > 0)
|
||||
if (data.length > 0)
|
||||
localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
useNodes(result.data?.nodes);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: useCache(result.data?.items)
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
export async function getCachedLibraries(cache: NormalisedCache, client: AlphaClient<Procedures>) {
|
||||
export async function getCachedLibraries(client: AlphaClient<Procedures>) {
|
||||
const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
|
||||
|
||||
const libraries = client.query(['library.list']).then((result) => {
|
||||
cache.withNodes(result.nodes);
|
||||
const libraries = cache.withCache(result.items);
|
||||
|
||||
localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(result));
|
||||
|
||||
return libraries;
|
||||
return result;
|
||||
});
|
||||
|
||||
if (cachedData) {
|
||||
// If we fail to load cached data, it's fine
|
||||
try {
|
||||
const data = JSON.parse(cachedData);
|
||||
cache.withNodes(data.nodes);
|
||||
return cache.withCache(data.items) as LibraryConfigWrapped[];
|
||||
return data as LibraryConfigWrapped[];
|
||||
} catch (e) {
|
||||
console.error("Error loading cached 'sd-library-list' data", e);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ export * from './core';
|
|||
export * from './utils';
|
||||
export * from './lib';
|
||||
export * from './form';
|
||||
export * from './cache';
|
||||
export * from './color';
|
||||
export * from './solid';
|
||||
export * from './explorer';
|
||||
|
|
|
@ -131,24 +131,8 @@ export function formatNumber(n: number) {
|
|||
export function insertLibrary(queryClient: QueryClient, library: LibraryConfigWrapped) {
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||
// The invalidation system beat us to it
|
||||
if (libraries.items.find((l: any) => l.__id === library.uuid)) return libraries;
|
||||
if ((libraries || []).find((l: any) => l.uuid === library.uuid)) return libraries;
|
||||
|
||||
return {
|
||||
items: [
|
||||
...(libraries.items || []),
|
||||
{
|
||||
__type: 'LibraryConfigWrapped',
|
||||
__id: library.uuid
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
...(libraries.nodes || []),
|
||||
{
|
||||
__type: 'LibraryConfigWrapped',
|
||||
__id: library.uuid,
|
||||
...library
|
||||
}
|
||||
]
|
||||
};
|
||||
return [library, ...libraries];
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue