Implement background worker to make search response faster and avoid hanging the UI when there are a lot of files. Implement better toast messages for transitional events that automatically disappear.
This commit is contained in:
+1
-3
@@ -49,9 +49,7 @@ async def main_start(app):
|
|||||||
max_workers=preview_workers, thread_name_prefix="cista-preview"
|
max_workers=preview_workers, thread_name_prefix="cista-preview"
|
||||||
)
|
)
|
||||||
# Larger pool for long-running but low-memory zip operations
|
# Larger pool for long-running but low-memory zip operations
|
||||||
app.ctx.zipexec = ThreadPoolExecutor(
|
app.ctx.zipexec = ThreadPoolExecutor(max_workers=32, thread_name_prefix="cista-zip")
|
||||||
max_workers=32, thread_name_prefix="cista-zip"
|
|
||||||
)
|
|
||||||
watching.start(app)
|
watching.start(app)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="store.error && !store.authInProgress" class="toast-message" @click="store.error = ''">
|
<div v-if="store.toast" class="toast-message" @click="store.clearToast()">
|
||||||
|
{{ store.toast }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="store.error && !store.authInProgress" class="toast-message status" @click="store.error = ''">
|
||||||
{{ store.error }}
|
{{ store.error }}
|
||||||
</div>
|
</div>
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
@@ -100,6 +103,7 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
|
|||||||
// Globally close search, clear errors on Escape
|
// Globally close search, clear errors on Escape
|
||||||
else if (keyup && event.key === 'Escape') {
|
else if (keyup && event.key === 'Escape') {
|
||||||
store.error = ''
|
store.error = ''
|
||||||
|
store.clearToast()
|
||||||
headerMain.value!.closeSearch(event)
|
headerMain.value!.closeSearch(event)
|
||||||
store.focusBreadcrumb()
|
store.focusBreadcrumb()
|
||||||
}
|
}
|
||||||
@@ -181,4 +185,8 @@ export type { Path }
|
|||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.toast-message.status {
|
||||||
|
background: #555;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const submit = async (ev: Event) => {
|
|||||||
try {
|
try {
|
||||||
if (form.passwordChange) {
|
if (form.passwordChange) {
|
||||||
if (!form.password) {
|
if (!form.password) {
|
||||||
store.error = '⚠️ Current password is required'
|
store.showToast('⚠️ Current password is required')
|
||||||
password.value!.focus()
|
password.value!.focus()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ const submit = async (ev: Event) => {
|
|||||||
close()
|
close()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const httpError = error as ISimpleError
|
const httpError = error as ISimpleError
|
||||||
store.error = httpError.message || '🛑 Unknown error'
|
store.showToast(httpError.message || '🛑 Unknown error')
|
||||||
} finally {
|
} finally {
|
||||||
confirmLoading.value = false
|
confirmLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const uploadFiles = (infiles: File[]) => {
|
|||||||
const uploadCloudFiles = (files: CloudFile[]) => {
|
const uploadCloudFiles = (files: CloudFile[]) => {
|
||||||
const dotfiles = files.filter(f => f.cloudName.includes('/.'))
|
const dotfiles = files.filter(f => f.cloudName.includes('/.'))
|
||||||
if (dotfiles.length) {
|
if (dotfiles.length) {
|
||||||
store.error = "Won't upload dotfiles"
|
store.showToast("Won't upload dotfiles")
|
||||||
console.log("Dotfiles omitted", dotfiles)
|
console.log("Dotfiles omitted", dotfiles)
|
||||||
files = files.filter(f => !f.cloudName.includes('/.'))
|
files = files.filter(f => !f.cloudName.includes('/.'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const loadUsers = async () => {
|
|||||||
users.value = data.users
|
users.value = data.users
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const httpError = e as ISimpleError
|
const httpError = e as ISimpleError
|
||||||
store.error = httpError.message || 'Failed to load users'
|
store.showToast(httpError.message || 'Failed to load users')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ const addUser = async () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const httpError = e as ISimpleError
|
const httpError = e as ISimpleError
|
||||||
store.error = httpError.message || 'Failed to add user'
|
store.showToast(httpError.message || 'Failed to add user')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ const toggleAdmin = async (user: User, event: Event) => {
|
|||||||
user.privileged = target.checked
|
user.privileged = target.checked
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const httpError = e as ISimpleError
|
const httpError = e as ISimpleError
|
||||||
store.error = httpError.message || 'Failed to update user'
|
store.showToast(httpError.message || 'Failed to update user')
|
||||||
target.checked = user.privileged // revert
|
target.checked = user.privileged // revert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ const renameUser = async (user: User) => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const httpError = e as ISimpleError
|
const httpError = e as ISimpleError
|
||||||
store.error = httpError.message || 'Failed to rename user'
|
store.showToast(httpError.message || 'Failed to rename user')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ const resetPassword = async (user: User) => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const httpError = e as ISimpleError
|
const httpError = e as ISimpleError
|
||||||
store.error = httpError.message || 'Failed to reset password'
|
store.showToast(httpError.message || 'Failed to reset password')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ const deleteUserAction = async (username: string) => {
|
|||||||
await loadUsers()
|
await loadUsers()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const httpError = e as ISimpleError
|
const httpError = e as ISimpleError
|
||||||
store.error = httpError.message || 'Failed to delete user'
|
store.showToast(httpError.message || 'Failed to delete user')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ const updateServerSettings = async () => {
|
|||||||
success.value = 'Server settings updated'
|
success.value = 'Server settings updated'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const httpError = e as ISimpleError
|
const httpError = e as ISimpleError
|
||||||
store.error = httpError.message || 'Failed to update settings'
|
store.showToast(httpError.message || 'Failed to update settings')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import { defineStore, type StateTree } from 'pinia'
|
|||||||
import { collator } from '@/utils'
|
import { collator } from '@/utils'
|
||||||
import { watchConnect, resumeWatching } from '@/repositories/WS'
|
import { watchConnect, resumeWatching } from '@/repositories/WS'
|
||||||
import { sorted, type SortOrder } from '@/utils/docsort'
|
import { sorted, type SortOrder } from '@/utils/docsort'
|
||||||
|
import SearchWorker from '@/workers/searchWorker?worker'
|
||||||
|
|
||||||
|
// Singleton search worker instance
|
||||||
|
let searchWorker: Worker | null = null
|
||||||
|
let searchId = 0
|
||||||
|
|
||||||
|
function getSearchWorker(): Worker {
|
||||||
|
if (!searchWorker) {
|
||||||
|
searchWorker = new SearchWorker()
|
||||||
|
}
|
||||||
|
return searchWorker
|
||||||
|
}
|
||||||
|
|
||||||
export const useMainStore = defineStore('main', {
|
export const useMainStore = defineStore('main', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
document: [] as Doc[],
|
document: [] as Doc[],
|
||||||
selected: new Set<FUID>([]),
|
selected: new Set<FUID>([]),
|
||||||
query: '' as string,
|
query: '' as string,
|
||||||
|
searchResults: [] as Doc[],
|
||||||
|
searchLoading: false,
|
||||||
fileExplorer: null as any,
|
fileExplorer: null as any,
|
||||||
error: '' as string,
|
error: '' as string, // Permanent status message (e.g., "Reconnecting...")
|
||||||
|
toast: '' as string, // Temporary toast (auto-dismisses)
|
||||||
|
toastTimeout: null as ReturnType<typeof setTimeout> | null,
|
||||||
connected: false,
|
connected: false,
|
||||||
authInProgress: false,
|
authInProgress: false,
|
||||||
cursor: '' as string,
|
cursor: '' as string,
|
||||||
@@ -61,6 +77,66 @@ export const useMainStore = defineStore('main', {
|
|||||||
loc.push(name)
|
loc.push(name)
|
||||||
}
|
}
|
||||||
this.document = docs
|
this.document = docs
|
||||||
|
// Sync documents to search worker
|
||||||
|
this.syncSearchWorker()
|
||||||
|
},
|
||||||
|
/** Show a temporary toast message that auto-dismisses */
|
||||||
|
showToast(message: string, duration = 3000) {
|
||||||
|
if (this.toastTimeout) {
|
||||||
|
clearTimeout(this.toastTimeout)
|
||||||
|
this.toastTimeout = null
|
||||||
|
}
|
||||||
|
this.toast = message
|
||||||
|
this.toastTimeout = setTimeout(() => {
|
||||||
|
this.toast = ''
|
||||||
|
this.toastTimeout = null
|
||||||
|
}, duration)
|
||||||
|
},
|
||||||
|
/** Clear the current toast immediately */
|
||||||
|
clearToast() {
|
||||||
|
if (this.toastTimeout) {
|
||||||
|
clearTimeout(this.toastTimeout)
|
||||||
|
this.toastTimeout = null
|
||||||
|
}
|
||||||
|
this.toast = ''
|
||||||
|
},
|
||||||
|
syncSearchWorker() {
|
||||||
|
const worker = getSearchWorker()
|
||||||
|
// Send plain data to worker (no class instances)
|
||||||
|
const docData = this.document.map(doc => ({
|
||||||
|
loc: doc.loc,
|
||||||
|
name: doc.name,
|
||||||
|
key: doc.key,
|
||||||
|
size: doc.size,
|
||||||
|
mtime: doc.mtime,
|
||||||
|
dir: doc.dir,
|
||||||
|
}))
|
||||||
|
worker.postMessage({ type: 'update', documents: docData })
|
||||||
|
},
|
||||||
|
search(query: string, loc: string) {
|
||||||
|
const worker = getSearchWorker()
|
||||||
|
const id = ++searchId
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
this.searchResults = []
|
||||||
|
this.searchLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
login(username: string, privileged: boolean) {
|
||||||
this.user.username = username
|
this.user.username = username
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
:path="props.path"
|
:path="props.path"
|
||||||
:documents="documents"
|
:documents="documents"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
||||||
<EmptyFolder :documents=documents :path=props.path />
|
<EmptyFolder :documents=documents :path=props.path />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
import { watchEffect, ref, computed, watch } from 'vue'
|
import { watchEffect, ref, computed, watch } from 'vue'
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import Router from '@/router/index'
|
import Router from '@/router/index'
|
||||||
import { needleFormat, localeIncludes, collator } from '@/utils'
|
import { collator } from '@/utils'
|
||||||
import { sorted, sortedGrouped } from '@/utils/docsort'
|
import { sorted, sortedGrouped } from '@/utils/docsort'
|
||||||
import FileExplorer from '@/components/FileExplorer.vue'
|
import FileExplorer from '@/components/FileExplorer.vue'
|
||||||
|
|
||||||
@@ -30,41 +31,34 @@ const props = defineProps<{
|
|||||||
path: Array<string>
|
path: Array<string>
|
||||||
query: string
|
query: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Trigger search when query changes
|
||||||
|
watch(
|
||||||
|
() => [props.query, props.path.join('/')] as const,
|
||||||
|
([query, loc]) => {
|
||||||
|
store.search(query, loc)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
const documents = computed(() => {
|
const documents = computed(() => {
|
||||||
const loc = props.path.join('/')
|
const loc = props.path.join('/')
|
||||||
const query = props.query
|
const query = props.query
|
||||||
// List the current location
|
|
||||||
|
// List the current location (no search)
|
||||||
if (!query) return sorted(
|
if (!query) return sorted(
|
||||||
store.document.filter(doc => doc.loc === loc),
|
store.document.filter(doc => doc.loc === loc),
|
||||||
store.prefs.sortListing,
|
store.prefs.sortListing,
|
||||||
)
|
)
|
||||||
// Find up to 100 newest documents that match the search
|
|
||||||
const needle = needleFormat(query)
|
// Search results from worker
|
||||||
let limit = 100
|
const docs = store.searchResults
|
||||||
let docs = []
|
|
||||||
for (const doc of store.recentDocuments) {
|
|
||||||
if (localeIncludes(doc.haystack, needle)) {
|
|
||||||
docs.push(doc)
|
|
||||||
if (--limit === 0) break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const locsub = loc + '/'
|
|
||||||
// Custom sort override in effect? Use grouped sorting to keep folders together
|
// Custom sort override in effect? Use grouped sorting to keep folders together
|
||||||
const order = store.prefs.sortFiltered
|
const order = store.prefs.sortFiltered
|
||||||
if (order) return sortedGrouped(docs, order)
|
if (order) return sortedGrouped(docs, order)
|
||||||
// Sort by relevance - current folder, then subfolders, then others
|
|
||||||
docs.sort((a, b) => (
|
// Results are already sorted by relevance in the worker
|
||||||
// @ts-ignore
|
|
||||||
(b.loc === loc) - (a.loc === loc) ||
|
|
||||||
// @ts-ignore
|
|
||||||
(b.loc.slice(0, locsub.length) === locsub) - (a.loc.slice(0, locsub.length) === locsub) ||
|
|
||||||
collator.compare(a.loc, b.loc) ||
|
|
||||||
// @ts-ignore
|
|
||||||
(a.type === 'file') - (b.type === 'file') ||
|
|
||||||
// @ts-ignore
|
|
||||||
b.name.includes(query) - a.name.includes(query) ||
|
|
||||||
collator.compare(a.name, b.name)
|
|
||||||
))
|
|
||||||
return docs
|
return docs
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,4 +86,15 @@ watch([() => props.path.join('/'), () => store.document.length], ([path, len], [
|
|||||||
text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
|
text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
.search-loading {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--accent-color, #007bff);
|
||||||
|
color: white;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// Search worker - runs search in background thread
|
||||||
|
// Receives document updates and search queries, returns incremental results
|
||||||
|
|
||||||
|
interface DocData {
|
||||||
|
loc: string
|
||||||
|
name: string
|
||||||
|
key: string
|
||||||
|
size: number
|
||||||
|
mtime: number
|
||||||
|
dir: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkerDoc extends DocData {
|
||||||
|
haystack: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchMessage {
|
||||||
|
type: 'search'
|
||||||
|
query: string
|
||||||
|
loc: string
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateMessage {
|
||||||
|
type: 'update'
|
||||||
|
documents: DocData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncomingMessage = SearchMessage | UpdateMessage
|
||||||
|
|
||||||
|
interface ResultMessage {
|
||||||
|
type: 'results'
|
||||||
|
docs: DocData[]
|
||||||
|
id: number
|
||||||
|
done: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker state
|
||||||
|
let documents: WorkerDoc[] = []
|
||||||
|
let recentDocuments: WorkerDoc[] = [] // Sorted by mtime descending
|
||||||
|
let currentSearchId = 0
|
||||||
|
|
||||||
|
// Haystack formatting (same as main thread utils)
|
||||||
|
function haystackFormat(str: string): string {
|
||||||
|
const based = str.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
||||||
|
return '^' + based + '$'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needle formatting
|
||||||
|
function needleFormat(query: string) {
|
||||||
|
const based = query.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
||||||
|
return { based, words: based.split(/\s+/) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if haystack includes needle
|
||||||
|
function localeIncludes(haystack: string, filter: { based: string; words: string[] }): boolean {
|
||||||
|
const { based, words } = filter
|
||||||
|
return haystack.includes(based) || (words && words.every(word => haystack.includes(word)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collator for sorting
|
||||||
|
const collator = new Intl.Collator('en', { sensitivity: 'base', numeric: true, usage: 'search' })
|
||||||
|
|
||||||
|
// Sort by mtime descending
|
||||||
|
function sortByRecent(docs: WorkerDoc[]): WorkerDoc[] {
|
||||||
|
return [...docs].sort((a, b) => b.mtime - a.mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yield control to check for new messages
|
||||||
|
function yieldControl(): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform search with incremental results
|
||||||
|
async function performSearch(query: string, loc: string, searchId: number) {
|
||||||
|
const needle = needleFormat(query)
|
||||||
|
const limit = 100
|
||||||
|
const batchSize = 1000
|
||||||
|
const results: WorkerDoc[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < recentDocuments.length && results.length < limit; i += batchSize) {
|
||||||
|
// Check if search was superseded
|
||||||
|
if (currentSearchId !== searchId) return
|
||||||
|
|
||||||
|
// Process batch
|
||||||
|
const end = Math.min(i + batchSize, recentDocuments.length)
|
||||||
|
for (let j = i; j < end && results.length < limit; j++) {
|
||||||
|
const doc = recentDocuments[j]!
|
||||||
|
if (localeIncludes(doc.haystack, needle)) {
|
||||||
|
results.push(doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post incremental results if we found any in this batch
|
||||||
|
if (results.length > 0 && currentSearchId === searchId) {
|
||||||
|
const sortedResults = sortResults(results, query, loc)
|
||||||
|
postMessage({
|
||||||
|
type: 'results',
|
||||||
|
docs: sortedResults.map(stripHaystack),
|
||||||
|
id: searchId,
|
||||||
|
done: false
|
||||||
|
} as ResultMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yield control between batches
|
||||||
|
if (i + batchSize < recentDocuments.length && results.length < limit) {
|
||||||
|
await yieldControl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post final results
|
||||||
|
if (currentSearchId === searchId) {
|
||||||
|
const sortedResults = sortResults(results, query, loc)
|
||||||
|
postMessage({
|
||||||
|
type: 'results',
|
||||||
|
docs: sortedResults.map(stripHaystack),
|
||||||
|
id: searchId,
|
||||||
|
done: true
|
||||||
|
} as ResultMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort results by relevance
|
||||||
|
function sortResults(docs: WorkerDoc[], query: string, loc: string): WorkerDoc[] {
|
||||||
|
const locsub = loc + '/'
|
||||||
|
return [...docs].sort((a, b) => (
|
||||||
|
// Current folder first
|
||||||
|
// @ts-ignore
|
||||||
|
(b.loc === loc) - (a.loc === loc) ||
|
||||||
|
// Then subfolders
|
||||||
|
// @ts-ignore
|
||||||
|
(b.loc.slice(0, locsub.length) === locsub) - (a.loc.slice(0, locsub.length) === locsub) ||
|
||||||
|
// Then by location
|
||||||
|
collator.compare(a.loc, b.loc) ||
|
||||||
|
// Files after folders
|
||||||
|
// @ts-ignore
|
||||||
|
(a.dir === false) - (b.dir === false) ||
|
||||||
|
// Exact name match first
|
||||||
|
// @ts-ignore
|
||||||
|
b.name.includes(query) - a.name.includes(query) ||
|
||||||
|
// Finally by name
|
||||||
|
collator.compare(a.name, b.name)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip haystack before sending back to main thread
|
||||||
|
function stripHaystack(doc: WorkerDoc): DocData {
|
||||||
|
const { haystack, ...rest } = doc
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming messages
|
||||||
|
self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
|
||||||
|
const msg = e.data
|
||||||
|
|
||||||
|
if (msg.type === 'update') {
|
||||||
|
// Update document list with haystacks
|
||||||
|
documents = msg.documents.map(doc => ({
|
||||||
|
...doc,
|
||||||
|
haystack: haystackFormat(doc.name)
|
||||||
|
}))
|
||||||
|
recentDocuments = sortByRecent(documents)
|
||||||
|
} else if (msg.type === 'search') {
|
||||||
|
currentSearchId = msg.id
|
||||||
|
if (msg.query) {
|
||||||
|
await performSearch(msg.query, msg.loc, msg.id)
|
||||||
|
} else {
|
||||||
|
// Empty query - no results needed (main thread handles folder listing)
|
||||||
|
postMessage({
|
||||||
|
type: 'results',
|
||||||
|
docs: [],
|
||||||
|
id: msg.id,
|
||||||
|
done: true
|
||||||
|
} as ResultMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user