diff --git a/.changeset/chilled-days-live.md b/.changeset/chilled-days-live.md new file mode 100644 index 000000000..d8fa488ce --- /dev/null +++ b/.changeset/chilled-days-live.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The application no longer lags when uploading thousands of files. diff --git a/.changeset/itchy-paws-call.md b/.changeset/itchy-paws-call.md new file mode 100644 index 000000000..75a5a8652 --- /dev/null +++ b/.changeset/itchy-paws-call.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/react-core': minor +--- + +Added useThrottledStateMap. diff --git a/.changeset/light-ravens-own.md b/.changeset/light-ravens-own.md new file mode 100644 index 000000000..ea0348cb0 --- /dev/null +++ b/.changeset/light-ravens-own.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/react-core': minor +--- + +Added `throttle`, often useful for mutation callbacks. diff --git a/apps/renterd/contexts/filesManager/types.ts b/apps/renterd/contexts/filesManager/types.ts index 40853b02f..b458b61fa 100644 --- a/apps/renterd/contexts/filesManager/types.ts +++ b/apps/renterd/contexts/filesManager/types.ts @@ -85,7 +85,8 @@ export type ExplorerMode = 'directory' | 'flat' export type UploadStatus = 'queued' | 'uploading' | 'processing' export type ObjectUploadData = ObjectData & { - upload?: MultipartUpload + multipartId?: string + multipartUpload?: MultipartUpload uploadStatus: UploadStatus uploadAbort?: () => Promise uploadFile?: File diff --git a/apps/renterd/contexts/filesManager/uploads.tsx b/apps/renterd/contexts/filesManager/uploads.tsx index 51555a13e..030561ad0 100644 --- a/apps/renterd/contexts/filesManager/uploads.tsx +++ b/apps/renterd/contexts/filesManager/uploads.tsx @@ -1,5 +1,9 @@ import { triggerErrorToast } from '@siafoundation/design-system' -import { useMutate } from '@siafoundation/react-core' +import { + throttle, + useMutate, + useThrottledStateMap, +} from '@siafoundation/react-core' import { useBuckets, useMultipartUploadAbort, @@ -10,8 +14,7 @@ import { } from '@siafoundation/renterd-react' import { Bucket, busObjectsRoute } from '@siafoundation/renterd-types' import { MiBToBytes, minutesInMilliseconds } from '@siafoundation/units' -import { throttle } from '@technically/lodash' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { MultipartUpload } from '../../lib/multipartUpload' import { FullPath, @@ -26,6 +29,7 @@ const maxConcurrentUploads = 5 const maxConcurrentPartsPerUpload = 5 const getMultipartUploadPartSize = (minShards: number) => MiBToBytes(4).times(minShards) +const checkAndStartUploadsInterval = 500 type Props = { activeDirectoryPath: string @@ -38,7 +42,8 @@ export function useUploads({ activeDirectoryPath }: Props) { const busUploadComplete = useMultipartUploadComplete() const busUploadCreate = useMultipartUploadCreate() const busUploadAbort = useMultipartUploadAbort() - const [uploadsMap, setUploadsMap] = useState({}) + const [uploadsMapRef, setUploadsMapRef, uploadsMap] = + useThrottledStateMap({}) const uploadSettings = useSettingsUpload({ config: { swr: { @@ -46,59 +51,66 @@ export function useUploads({ activeDirectoryPath }: Props) { }, }, }) + const uploadsList: ObjectUploadData[] = useMemo( + () => Object.values(uploadsMap), + [uploadsMap] + ) + + const hasUploads = uploadsList.length > 0 const updateStatusToUploading = useCallback( - ({ id }: { id: string }) => { - setUploadsMap((map) => ({ - ...map, - [id]: { - ...map[id], + ({ id, multipartId }: { id: string; multipartId: string }) => { + setUploadsMapRef((current) => { + current[id] = { + ...current[id], + multipartId, uploadStatus: 'uploading', loaded: 0, - }, - })) + } + return current + }) }, - [setUploadsMap] + [setUploadsMapRef] ) const updateUploadProgress = useCallback( (obj: { id: string; loaded: number; size: number }) => { - setUploadsMap((map) => { - if (!map[obj.id]) { - return map + setUploadsMapRef((current) => { + const upload = current[obj.id] + if (!upload || upload.loaded === obj.loaded) { + return current } - return { - ...map, - [obj.id]: { - ...map[obj.id], - loaded: obj.loaded, - uploadStatus: obj.loaded === obj.size ? 'processing' : 'uploading', - size: obj.size, - }, + + current[obj.id] = { + ...upload, + loaded: obj.loaded, + uploadStatus: obj.loaded === obj.size ? 'processing' : 'uploading', + size: obj.size, } + return current }) }, - [setUploadsMap] + [setUploadsMapRef] ) const removeUpload = useCallback( (id: string) => { - setUploadsMap((uploads) => { - delete uploads[id] - return { - ...uploads, - } + setUploadsMapRef((current) => { + delete current[id] + return current }) }, - [setUploadsMap] + [setUploadsMapRef] ) - const createMultipartUpload = useCallback( + const initMultipartUpload = useCallback( async ({ + id, path, bucket, uploadFile, }: { + id: string path: FullPath bucket: Bucket uploadFile: File @@ -115,129 +127,129 @@ export function useUploads({ activeDirectoryPath }: Props) { maxConcurrentParts: maxConcurrentPartsPerUpload, }) - const uploadId = await multipartUpload.create() - if (!uploadId) { - triggerErrorToast({ - title: 'Error creating upload', - body: 'Failed to create upload', - }) - return - } multipartUpload.setOnError((error) => { triggerErrorToast({ title: 'Error uploading file', body: error.message, }) - ref.current.removeUpload(uploadId) + ref.current.removeUpload(id) + }) + multipartUpload.setOnProgress((progress) => { + ref.current.updateUploadProgress({ + id, + loaded: progress.sent, + size: progress.total, + }) }) - multipartUpload.setOnProgress( - throttle((progress) => { - ref.current.updateUploadProgress({ - id: uploadId, - loaded: progress.sent, - size: progress.total, - }) - }, 1000) - ) multipartUpload.setOnComplete(async () => { - await ref.current.mutate((key) => key.startsWith(busObjectsRoute)) - ref.current.removeUpload(uploadId) - setTimeout(() => { - ref.current.checkAndStartUploads() - }, 100) + throttle(busObjectsRoute, 5000, () => + mutate((key) => key.startsWith(busObjectsRoute)) + ) + ref.current.removeUpload(id) }) - return { - uploadId, - multipartUpload, - } + return multipartUpload }, - [uploadSettings.data] + [uploadSettings.data, mutate] ) const addUploadToQueue = useCallback( async ({ + id, path, bucket, name, uploadFile, }: { + id: string path: FullPath bucket: Bucket name: string uploadFile: File }) => { - const upload = await createMultipartUpload({ + const multipartUpload = await initMultipartUpload({ + id, path, bucket, uploadFile, }) - if (!upload) { + if (!multipartUpload) { return } - const { uploadId, multipartUpload } = upload - setUploadsMap((map) => { - const key = getKeyFromPath(path) - const uploadItem: ObjectUploadData = { - id: uploadId, - path, - key, - bucket, - name, - size: uploadFile.size, - loaded: 0, - isUploading: true, - upload: multipartUpload, - uploadStatus: 'queued', - uploadFile: uploadFile, - createdAt: new Date().toISOString(), - uploadAbort: async () => { - await multipartUpload.abort() - ref.current.removeUpload(uploadId) - }, - type: 'file', - } - return { - ...map, - [uploadId]: uploadItem, - } + const key = getKeyFromPath(path) + const uploadItem: ObjectUploadData = { + id, + path, + key, + bucket, + name, + size: uploadFile.size, + loaded: 0, + isUploading: true, + multipartId: undefined, + multipartUpload, + uploadStatus: 'queued', + uploadFile: uploadFile, + createdAt: new Date().toISOString(), + uploadAbort: async () => { + ref.current.removeUpload(id) + multipartUpload.abort() + }, + type: 'file', + } + setUploadsMapRef((current) => { + current[id] = uploadItem + return current }) }, - [setUploadsMap, createMultipartUpload] + [initMultipartUpload, setUploadsMapRef] ) const startMultipartUpload = useCallback( async ({ id, upload }: { id: string; upload: MultipartUpload }) => { + const multipartId = await upload.create() + if (!multipartId) { + triggerErrorToast({ + title: 'Error creating upload', + body: 'Failed to create upload', + }) + return + } updateStatusToUploading({ id, + multipartId, }) - upload.start() + await upload.start() }, [updateStatusToUploading] ) - const checkAndStartUploads = useCallback(() => { - const uploads = Object.values(uploadsMap) - const activeUploads = uploads.filter( - (upload) => upload.uploadStatus === 'uploading' - ).length - const queuedUploads = uploads.filter( - (upload) => upload.uploadStatus === 'queued' - ) - - const availableSlots = maxConcurrentUploads - activeUploads + const checkAndStartUploads = useCallback( + () => + throttle('checkAndStartUploads', checkAndStartUploadsInterval, () => { + const uploadsListRef = Object.values(uploadsMapRef) + const activeUploads = uploadsListRef.filter( + (upload) => upload.uploadStatus === 'uploading' + ) + const queuedUploads = uploadsListRef.filter( + (upload) => upload.uploadStatus === 'queued' + ) - // Start uploads if there are available slots and queued uploads. - queuedUploads.slice(0, availableSlots).forEach((upload) => { - if (!upload.upload) { - return - } - startMultipartUpload({ - id: upload.id, - upload: upload.upload, - }) - }) - return uploadsMap - }, [uploadsMap, startMultipartUpload]) + const availableSlots = Math.max( + maxConcurrentUploads - activeUploads.length, + 0 + ) + queuedUploads.slice(0, availableSlots).forEach((upload) => { + if (!upload.multipartUpload) { + return + } + startMultipartUpload({ + id: upload.id, + upload: upload.multipartUpload, + }) + }) + }), + [uploadsMapRef, startMultipartUpload] + ) const uploadFiles = useCallback( (files: File[]) => { @@ -249,6 +261,7 @@ export function useUploads({ activeDirectoryPath }: Props) { // Try `path` otherwise fallback to flat file structure. const relativeUserFilePath = (file['path'] as string) || file.name const path = join(activeDirectoryPath, relativeUserFilePath) + const id = getUploadId(path) const name = file.name const bucketName = getBucketFromPath(path) const bucket = buckets.data?.find((b) => b.name === bucketName) @@ -259,35 +272,33 @@ export function useUploads({ activeDirectoryPath }: Props) { }) return } - if (uploadsMap[path]) { + if (uploadsMapRef[id]) { triggerErrorToast({ title: `Already uploading file, aborting previous upload.`, body: path, }) - uploadsMap[path].uploadAbort?.() + uploadsMapRef[id].uploadAbort?.() } addUploadToQueue({ + id, path, name, bucket, uploadFile: file, }) }) - setTimeout(() => { - ref.current.checkAndStartUploads() - }, 1_000) }, - [activeDirectoryPath, addUploadToQueue, buckets.data, uploadsMap] + [activeDirectoryPath, addUploadToQueue, buckets.data, uploadsMapRef] ) // Use a ref for functions that will be used in closures/asynchronous callbacks // to ensure the latest version of the function is used. const ref = useRef({ checkAndStartUploads, - workerUploadPart: workerUploadPart, - busUploadComplete: busUploadComplete, - busUploadCreate: busUploadCreate, - busUploadAbort: busUploadAbort, + workerUploadPart, + busUploadComplete, + busUploadCreate, + busUploadAbort, removeUpload, updateUploadProgress, updateStatusToUploading, @@ -319,21 +330,16 @@ export function useUploads({ activeDirectoryPath }: Props) { ]) useEffect(() => { - const i = setInterval(() => { - ref.current.checkAndStartUploads() - }, 3_000) - return () => { - clearInterval(i) + if (hasUploads) { + const id = setInterval(() => { + ref.current.checkAndStartUploads() + }, checkAndStartUploadsInterval) + return () => clearInterval(id) } - }, []) - - const uploadsList: ObjectUploadData[] = useMemo( - () => Object.entries(uploadsMap).map((u) => u[1] as ObjectUploadData), - [uploadsMap] - ) + }, [hasUploads]) // Abort local uploads when the browser tab is closed. - useWarnActiveUploadsOnClose({ uploadsMap }) + useWarnActiveUploadsOnClose({ uploadsList }) return { uploadFiles, @@ -341,3 +347,7 @@ export function useUploads({ activeDirectoryPath }: Props) { uploadsList, } } + +export function getUploadId(path: FullPath) { + return `u/${path}` +} diff --git a/apps/renterd/contexts/filesManager/useWarnActiveUploadsOnClose.tsx b/apps/renterd/contexts/filesManager/useWarnActiveUploadsOnClose.tsx index ce7a0241a..bc5cf57f4 100644 --- a/apps/renterd/contexts/filesManager/useWarnActiveUploadsOnClose.tsx +++ b/apps/renterd/contexts/filesManager/useWarnActiveUploadsOnClose.tsx @@ -1,30 +1,26 @@ import { useEffect } from 'react' -import { UploadsMap } from './types' +import { ObjectUploadData } from './types' export function useWarnActiveUploadsOnClose({ - uploadsMap, + uploadsList, }: { - uploadsMap: UploadsMap + uploadsList: ObjectUploadData[] }) { useEffect(() => { - const activeUploads = Object.values(uploadsMap).filter( - (upload) => upload.uploadStatus === 'uploading' - ) - const warnUserAboutActiveUploads = (event: BeforeUnloadEvent) => { - if (activeUploads.length > 0) { - const message = `Warning, closing the tab will abort all ${activeUploads.length} active uploads.` + if (uploadsList.length > 0) { + const message = `Warning, closing the tab will abort all ${uploadsList.length} active uploads.` event.returnValue = message // Legacy method for cross browser support return message // Chrome requires returnValue to be set } } - if (activeUploads.length > 0) { + if (uploadsList.length > 0) { window.addEventListener('beforeunload', warnUserAboutActiveUploads) } return () => { window.removeEventListener('beforeunload', warnUserAboutActiveUploads) } - }, [uploadsMap]) + }, [uploadsList]) } diff --git a/apps/renterd/contexts/uploads/index.tsx b/apps/renterd/contexts/uploads/index.tsx index 2b4c9c05b..848253fa3 100644 --- a/apps/renterd/contexts/uploads/index.tsx +++ b/apps/renterd/contexts/uploads/index.tsx @@ -17,6 +17,7 @@ import { ObjectUploadData } from '../filesManager/types' import { MultipartUploadListUploadsPayload } from '@siafoundation/renterd-types' import { maybeFromNullishArrayResponse } from '@siafoundation/react-core' import { Maybe, Nullable } from '@siafoundation/types' +import { getUploadId } from '../filesManager/uploads' const defaultLimit = 50 @@ -84,7 +85,7 @@ function useUploadsMain() { const key = upload.key const name = getFilename(key) const fullPath = join(activeBucket.name, upload.key) - const localUpload = uploadsMap[id] + const localUpload = uploadsMap[getUploadId(fullPath)] if (localUpload) { return localUpload } diff --git a/apps/renterd/lib/multipartUpload.ts b/apps/renterd/lib/multipartUpload.ts index 1575fa349..61d2f0101 100644 --- a/apps/renterd/lib/multipartUpload.ts +++ b/apps/renterd/lib/multipartUpload.ts @@ -136,19 +136,21 @@ export class MultipartUpload { this.#activeConnections[id].abort() }) - try { - await this.#api.busUploadAbort.post({ - payload: { - bucket: this.#bucket, - key: this.#key, - uploadID: this.#uploadId, - } as MultipartUploadAbortPayload, - }) - } catch (e) { - triggerErrorToast({ - title: 'Error aborting upload', - body: (e as Error).message, - }) + if (this.#uploadId) { + try { + await this.#api.busUploadAbort.post({ + payload: { + bucket: this.#bucket, + key: this.#key, + uploadID: this.#uploadId, + } as MultipartUploadAbortPayload, + }) + } catch (e) { + triggerErrorToast({ + title: 'Error aborting upload', + body: (e as Error).message, + }) + } } this.#resolve?.() } diff --git a/libs/react-core/package.json b/libs/react-core/package.json index 168f4be5a..78be4a79e 100644 --- a/libs/react-core/package.json +++ b/libs/react-core/package.json @@ -5,7 +5,8 @@ "license": "MIT", "peerDependencies": { "react": "^18.2.0", - "@siafoundation/types": "0.7.0" + "@siafoundation/types": "0.7.0", + "@technically/lodash": "^4.17.0" }, "dependencies": { "@siafoundation/next": "^0.1.3", diff --git a/libs/react-core/src/index.ts b/libs/react-core/src/index.ts index aa2e9b4a2..46c5e3fa1 100644 --- a/libs/react-core/src/index.ts +++ b/libs/react-core/src/index.ts @@ -14,6 +14,8 @@ export * from './useTryUntil' export * from './userPrefersReducedMotion' export * from './mutate' export * from './arrayResponse' +export * from './throttle' +export * from './useThrottledStateMap' export * from './workflows' export * from './coreProvider' diff --git a/libs/react-core/src/throttle.test.ts b/libs/react-core/src/throttle.test.ts new file mode 100644 index 000000000..3cc5a3b2e --- /dev/null +++ b/libs/react-core/src/throttle.test.ts @@ -0,0 +1,62 @@ +import { throttle } from './throttle' +import { jest, describe, it, expect, beforeEach } from '@jest/globals' + +describe('throttle', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('both', () => { + const fn = jest.fn() + throttle('test', 100, fn) + throttle('test', 100, fn) + throttle('test', 100, fn) + + expect(fn).toHaveBeenCalledTimes(1) + + jest.runAllTimers() + + expect(fn).toHaveBeenCalledTimes(2) + }) + + it('trailing', () => { + const fn = jest.fn() + throttle('test', 100, fn, 'trailing') + throttle('test', 100, fn, 'trailing') + throttle('test', 100, fn, 'trailing') + + expect(fn).toHaveBeenCalledTimes(0) + + jest.runAllTimers() + + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('leading', () => { + const fn = jest.fn() + throttle('test', 100, fn, 'leading') + throttle('test', 100, fn, 'leading') + throttle('test', 100, fn, 'leading') + + expect(fn).toHaveBeenCalledTimes(1) + + jest.runAllTimers() + + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('should use the last cached function', () => { + const fn1 = jest.fn() + const fn2 = jest.fn() + throttle('test2', 100, fn1, 'trailing') + throttle('test2', 100, fn2, 'trailing') + + jest.runAllTimers() + + expect(fn2).toHaveBeenCalledTimes(1) + }) +}) diff --git a/libs/react-core/src/throttle.ts b/libs/react-core/src/throttle.ts new file mode 100644 index 000000000..30ce4e21e --- /dev/null +++ b/libs/react-core/src/throttle.ts @@ -0,0 +1,33 @@ +import { throttle as _throttle } from '@technically/lodash' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const wrapperRegistry = new Map any>() + +/** + * Executes a function with throttling. Creates and caches the throttled version + * of the function based on the key and delay. + * + * @param key - Unique identifier for the throttled function + * @param delay - Throttle delay in ms + * @param fn - Function to throttle + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function throttle any>( + key: string, + delay: number, + fn: T, + edge: 'leading' | 'trailing' | 'both' = 'both' +): (...args: Parameters) => ReturnType { + const fullKey = key + edge + String(delay) + if (!wrapperRegistry.has(fullKey)) { + wrapperRegistry.set( + fullKey, + _throttle((func: T, ...args: Parameters) => func(...args), delay, { + leading: edge === 'leading' || edge === 'both', + trailing: edge === 'trailing' || edge === 'both', + }) + ) + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return wrapperRegistry.get(fullKey)!(fn) +} diff --git a/libs/react-core/src/useThrottledStateMap.ts b/libs/react-core/src/useThrottledStateMap.ts new file mode 100644 index 000000000..33eff3c8d --- /dev/null +++ b/libs/react-core/src/useThrottledStateMap.ts @@ -0,0 +1,59 @@ +'use client' + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { throttle } from '@technically/lodash' + +/** + * A hook that allows you to create a state map that can be updated thousands + * of times per second directly with the setter function, but only updates the + * render state every `throttleMs` milliseconds. + * + * @param initialValue - The initial value of the map. + * @param throttleMs - The throttle time in milliseconds. + * @returns A tuple containing the current value of the map, a setter function, + * and the throttled value. + */ +export function useThrottledStateMap>( + initialValue: T, + throttleMs = 1000 +): [T, (value: T | ((prev: T) => T)) => void, T] { + // Real-time value in ref. + const valueRef = useRef(initialValue) + // Throttled state for renders. + const [throttledValue, setThrottledValue] = useState(initialValue) + + // Throttled sync function. + const sync = useMemo( + () => + throttle( + () => { + setThrottledValue({ ...valueRef.current }) + }, + throttleMs, + { + trailing: true, + } + ), + [throttleMs] + ) + + // Cleanup throttle on unmount. + useEffect(() => { + return () => { + sync.cancel() + } + }, [sync]) + + // Setter that updates ref and triggers throttled sync. + const setValue = useCallback( + (value: T | ((prev: T) => T)) => { + const nextValue = + value instanceof Function ? value(valueRef.current) : value + valueRef.current = nextValue + sync() + }, + [sync] + ) + + return [valueRef.current, setValue, throttledValue] +} diff --git a/libs/renterd-react/package.json b/libs/renterd-react/package.json index 73a46ba1b..4c6e5ca66 100644 --- a/libs/renterd-react/package.json +++ b/libs/renterd-react/package.json @@ -4,7 +4,6 @@ "version": "4.13.3", "license": "MIT", "dependencies": { - "@technically/lodash": "^4.17.0", "@siafoundation/react-core": "^1.6.0", "swr": "^2.1.1", "@siafoundation/renterd-types": "0.14.1", diff --git a/libs/renterd-react/src/bus.ts b/libs/renterd-react/src/bus.ts index 57a798883..1e8c02a9a 100644 --- a/libs/renterd-react/src/bus.ts +++ b/libs/renterd-react/src/bus.ts @@ -1,5 +1,4 @@ import useSWR from 'swr' -import { debounce } from '@technically/lodash' import { useDeleteFunc, useGetSwr, @@ -10,6 +9,7 @@ import { HookArgsCallback, HookArgsWithPayloadSwr, delay, + throttle, } from '@siafoundation/react-core' import { getMainnetBlockHeight, @@ -558,8 +558,6 @@ export function useHostResetLostSectorCount( }) } -const debouncedListRevalidate = debounce((func: () => void) => func(), 5_000) - export function useHostScan( args?: HookArgsCallback ) { @@ -594,12 +592,8 @@ export function useHostScan( }), false ) - debouncedListRevalidate(() => { - mutate( - (key) => key.startsWith(busHostsRoute), - (d) => d, - true - ) + throttle(busHostsRoute, 5_000, () => { + mutate((key) => key.startsWith(busHostsRoute)) }) } ) @@ -935,8 +929,10 @@ export function useMultipartUploadCreate( return usePostFunc( { ...args, route: busMultipartCreateRoute }, async (mutate) => { - mutate((key) => { - return key.startsWith(busMultipartRoute) + throttle(busMultipartRoute, 3_000, () => { + mutate((key) => { + return key.startsWith(busMultipartRoute) + }) }) } ) @@ -951,9 +947,11 @@ export function useMultipartUploadComplete( ) { return usePostFunc( { ...args, route: busMultipartCompleteRoute }, - async (mutate) => { - mutate((key) => { - return key.startsWith(busMultipartRoute) + async (mutate, args, response) => { + throttle(busMultipartRoute, 3_000, () => { + mutate((key) => { + return key.startsWith(busMultipartRoute) + }) }) } ) @@ -969,9 +967,16 @@ export function useMultipartUploadAbort( return usePostFunc( { ...args, route: busMultipartAbortRoute }, async (mutate) => { - mutate((key) => { - return key.startsWith(busMultipartRoute) - }) + throttle( + busMultipartRoute, + 200, + () => { + mutate((key) => { + return key.startsWith(busMultipartRoute) + }) + }, + 'trailing' + ) } ) } diff --git a/package-lock.json b/package-lock.json index a42f846d7..0e76a5107 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,7 +108,6 @@ "use-debounce": "^9.0.3", "use-local-storage-state": "^18.3.3", "usehooks-ts": "^2.9.1", - "uuid": "^9.0.0", "yup": "^0.32.11" }, "devDependencies": { @@ -511,6 +510,7 @@ }, "peerDependencies": { "@siafoundation/types": "0.7.0", + "@technically/lodash": "^4.17.0", "react": "^18.2.0" } }, @@ -591,7 +591,6 @@ "@siafoundation/react-core": "^1.6.0", "@siafoundation/renterd-types": "0.14.1", "@siafoundation/units": "3.3.0", - "@technically/lodash": "^4.17.0", "swr": "^2.1.1" } }, @@ -30791,14 +30790,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/uvu": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz", @@ -38310,7 +38301,6 @@ "@siafoundation/react-core": "^1.6.0", "@siafoundation/renterd-types": "0.14.1", "@siafoundation/units": "3.3.0", - "@technically/lodash": "^4.17.0", "swr": "^2.1.1" } }, @@ -52816,11 +52806,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, "uvu": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz", diff --git a/package.json b/package.json index 2be134e0c..c7edea822 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,6 @@ "use-debounce": "^9.0.3", "use-local-storage-state": "^18.3.3", "usehooks-ts": "^2.9.1", - "uuid": "^9.0.0", "yup": "^0.32.11" }, "devDependencies": {