Optimizing the search faster; don't trigger full re render by URL changes.

This commit is contained in:
Leo Vasanko
2026-01-31 22:17:05 +00:00
parent 79875d3190
commit d1440f539b
5 changed files with 48 additions and 27 deletions
+10 -2
View File
@@ -51,14 +51,22 @@ const closeSearch = (ev: Event) => {
breadcrumb.focus()
updateSearch(ev)
}
const updateSearch = (ev: Event) => {
const q = (ev.target as HTMLInputElement).value
let p = props.path.join('/')
p = p ? `/${p}` : ''
const url = q ? `${p}//${q}` : (p || '/')
const u = url.replaceAll('?', '%3F').replaceAll('#', '%23')
if (!props.query && q) router.push(u)
else router.replace(u)
// Start search immediately via store (worker handles it async)
store.search(q, props.path.join('/'))
// Update route in next frame to keep typing responsive
requestAnimationFrame(() => {
if (!props.query && q) router.push(u)
else router.replace(u)
})
}
const toggleSearchInput = (ev: Event) => {
showSearchInput.value = !showSearchInput.value
+2 -4
View File
@@ -1,4 +1,4 @@
import { formatSize, formatUnixDate, haystackFormat } from "@/utils"
import { formatSize, formatUnixDate } from "@/utils"
export type FUID = string
@@ -16,7 +16,6 @@ export class Doc {
public key: FUID = ""
public size: number = 0
public mtime: number = 0
public haystack: string = ""
public dir: boolean = false
/** @internal Use the name getter/setter instead */
public _name: string = ""
@@ -24,13 +23,12 @@ export class Doc {
constructor(props: Partial<DocProps> = {}) {
const { name, ...rest } = props
Object.assign(this, rest)
if (name) this.name = name // Use setter for validation
if (name) this._name = name // Skip validation/haystack for bulk loading
}
get name() { return this._name }
set name(name: string) {
if (name.includes('/') || name.startsWith('.')) throw Error(`Invalid name: ${name}`)
this._name = name
this.haystack = haystackFormat(name)
}
get sizedisp(): string { return formatSize(this.size) }
get modified(): string { return formatUnixDate(this.mtime) }
+18 -12
View File
@@ -9,10 +9,26 @@ import SearchWorker from '@/workers/searchWorker?worker'
// Singleton search worker instance
let searchWorker: Worker | null = null
let searchId = 0
let searchStore: ReturnType<typeof useMainStore> | null = null
function getSearchWorker(): Worker {
if (!searchWorker) {
searchWorker = new SearchWorker()
// Set up message handler once
searchWorker.onmessage = (e) => {
if (!searchStore || e.data.id !== searchId) return // Stale result
// Convert plain data back to Doc instances (constructor is now lightweight)
const docs = []
for (const d of e.data.docs) {
docs.push(new Doc(d))
}
searchStore.searchResults = docs
if (e.data.done) {
searchStore.searchLoading = false
}
}
}
return searchWorker
}
@@ -24,6 +40,7 @@ export const useMainStore = defineStore('main', {
query: '' as string,
searchResults: [] as Doc[],
searchLoading: false,
_searchRouteTimer: null as ReturnType<typeof setTimeout> | null,
fileExplorer: null as any,
error: '' as string, // Permanent status message (e.g., "Reconnecting...")
toast: '' as string, // Temporary toast (auto-dismisses)
@@ -116,6 +133,7 @@ export const useMainStore = defineStore('main', {
search(query: string, loc: string) {
const worker = getSearchWorker()
const id = ++searchId
searchStore = this // Store reference for worker callback
if (!query) {
this.searchResults = []
@@ -124,18 +142,6 @@ export const useMainStore = defineStore('main', {
}
this.searchLoading = true
worker.onmessage = (e) => {
if (e.data.id !== searchId) return // Stale result
// Convert plain data back to Doc instances
this.searchResults = e.data.docs.map((d: any) => new Doc(d))
if (e.data.done) {
this.searchLoading = false
}
}
worker.postMessage({ type: 'search', query, loc, id })
},
login(username: string, privileged: boolean) {
+12 -5
View File
@@ -2,14 +2,14 @@
<Gallery
v-if="store.prefs.gallery"
ref="fileExplorer"
:key="`gallery-${Router.currentRoute.value.path}`"
:key="`gallery-${folderPath}`"
:path="props.path"
:documents="documents"
/>
<FileExplorer
v-else
ref="fileExplorer"
:key="`explorer-${Router.currentRoute.value.path}`"
:key="`explorer-${folderPath}`"
:path="props.path"
:documents="documents"
/>
@@ -20,7 +20,6 @@
<script setup lang="ts">
import { watchEffect, ref, computed, watch } from 'vue'
import { useMainStore } from '@/stores/main'
import Router from '@/router/index'
import { collator } from '@/utils'
import { sorted, sortedGrouped } from '@/utils/docsort'
import FileExplorer from '@/components/FileExplorer.vue'
@@ -32,11 +31,19 @@ const props = defineProps<{
query: string
}>()
// Trigger search when query changes
// Folder path for component keys - only recreate component when folder changes, not search
const folderPath = computed(() => props.path.join('/'))
// Trigger search when query changes (from route, e.g., page load or back button)
// Note: Direct typing triggers search immediately via HeaderMain, this is for route-based changes
watch(
() => [props.query, props.path.join('/')] as const,
([query, loc]) => {
store.search(query, loc)
// Only trigger if results don't match current query (avoid duplicate searches)
if (query && store.searchResults.length === 0) {
store.search(query, loc)
} else if (!query) {
store.search('', loc) // Clear search results
}
},
{ immediate: true }
)
+6 -4
View File
@@ -75,8 +75,9 @@ function yieldControl(): Promise<void> {
async function performSearch(query: string, loc: string, searchId: number) {
const needle = needleFormat(query)
const limit = 100
const batchSize = 1000
const batchSize = 500 // Smaller batches for faster incremental feedback
const results: WorkerDoc[] = []
let lastResultCount = 0
for (let i = 0; i < recentDocuments.length && results.length < limit; i += batchSize) {
// Check if search was superseded
@@ -91,8 +92,9 @@ async function performSearch(query: string, loc: string, searchId: number) {
}
}
// Post incremental results if we found any in this batch
if (results.length > 0 && currentSearchId === searchId) {
// Post incremental results if we found new matches
if (results.length > lastResultCount && currentSearchId === searchId) {
lastResultCount = results.length
const sortedResults = sortResults(results, query, loc)
postMessage({
type: 'results',
@@ -102,7 +104,7 @@ async function performSearch(query: string, loc: string, searchId: number) {
} as ResultMessage)
}
// Yield control between batches
// Yield control between batches to allow new search requests to interrupt
if (i + batchSize < recentDocuments.length && results.length < limit) {
await yieldControl()
}