Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ const config = [
'@typescript-eslint': tseslint.plugin
},
rules: {
'@typescript-eslint/no-unsafe-argument': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'warn',
'@typescript-eslint/no-unsafe-call': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ ignoreRestSiblings: true }
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dependencies": {
"comlink": "^4.4.1",
"cozy-app-publish": "^0.34.0",
"cozy-client": "^49.0.0",
"cozy-client": "^49.8.0",
"cozy-device-helper": "^3.1.0",
"cozy-flags": "^4.0.0",
"cozy-logger": "^1.10.4",
Expand All @@ -37,6 +37,7 @@
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@types/jest": "^29.5.13",
"@types/pouchdb-browser": "^6.1.5",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/sharedworker": "^0.0.126",
Expand Down
4 changes: 2 additions & 2 deletions src/@types/cozy-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,14 @@ declare module 'cozy-client' {
}

export default class CozyClient {
plugins: any
plugins: unknown
constructor(rawOptions?: ClientOptions)
getStackClient(): StackClient
getInstanceOptions(): InstanceOptions
instanceOptions: InstanceOptions
collection(doctype: string): Collection
isLogged: boolean
on: (event: string, callback: () => void) => void
on: (event: string, callback: (doctype: string) => void) => void
removeListener: (event: string, callback: () => void) => void
logout: () => Promise<void>
query: (
Expand Down
3 changes: 3 additions & 0 deletions src/@types/cozy-realtime.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'cozy-realtime' {
export const RealtimePlugin = (): null => null
}
6 changes: 4 additions & 2 deletions src/dataproxy/worker/platformWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ const openDB = (): Promise<IDBDatabase> => {
}

const storage = {
getItem: async (key: string): Promise<any> => {
getItem: async (key: string): Promise<unknown> => {
const db = await openDB()
return new Promise((resolve, reject) => {
const transaction = db.transaction('store', 'readonly')
const store = transaction.objectStore('store')
const request = store.get(key)

request.onsuccess = (): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
resolve(request.result ? request.result.value : null)
}

Expand All @@ -48,7 +49,7 @@ const storage = {
})
},

setItem: async (key: string, value: any): Promise<void> => {
setItem: async (key: string, value: unknown): Promise<void> => {
const db = await openDB()
return new Promise((resolve, reject) => {
const transaction = db.transaction('store', 'readwrite')
Expand Down Expand Up @@ -105,6 +106,7 @@ const isOnline = async (): Promise<boolean> => {
export const platformWorker = {
storage,
events,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
pouchAdapter: PouchDB,
isOnline
}
70 changes: 48 additions & 22 deletions src/search/SearchEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
LIMIT_DOCTYPE_SEARCH,
REPLICATION_DEBOUNCE,
ROOT_DIR_ID,
SHARED_DRIVES_DIR_ID
SHARED_DRIVES_DIR_ID,
SearchedDoctype
} from '@/search/consts'
import { getPouchLink } from '@/search/helpers/client'
import { getSearchEncoder } from '@/search/helpers/getSearchEncoder'
Expand All @@ -30,10 +31,10 @@ import {
isIOCozyApp,
isIOCozyContact,
isIOCozyFile,
SearchedDoctype,
SearchIndex,
SearchIndexes,
SearchResult
SearchResult,
isSearchedDoctype
} from '@/search/types'

const log = Minilog('🗂️ [Indexing]')
Expand All @@ -50,7 +51,7 @@ class SearchEngine {

constructor(client: CozyClient) {
this.client = client
this.searchIndexes = {}
this.searchIndexes = {} as SearchIndexes

this.indexOnChanges()
this.debouncedReplication = startReplicationWithDebounce(
Expand All @@ -64,7 +65,9 @@ class SearchEngine {
return
}
this.client.on('pouchlink:doctypesync:end', async (doctype: string) => {
await this.indexDocsForSearch(doctype)
if (isSearchedDoctype(doctype)) {
await this.indexDocsForSearch(doctype as keyof typeof SEARCH_SCHEMA)
}
})
this.client.on('login', () => {
// Ensure login is done before plugin register
Expand All @@ -79,15 +82,17 @@ class SearchEngine {
}

subscribeDoctype(client: CozyClient, doctype: string): void {
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
const realtime = this.client.plugins.realtime
realtime.subscribe('created', doctype, this.handleUpdatedOrCreatedDoc)
realtime.subscribe('updated', doctype, this.handleUpdatedOrCreatedDoc)
realtime.subscribe('deleted', doctype, this.handleDeletedDoc)
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
}

handleUpdatedOrCreatedDoc(doc: CozyDoc): void {
const doctype: string | undefined = doc._type
if (!doctype) {
const doctype = doc._type
if (!isSearchedDoctype(doctype)) {
return
}
const searchIndex = this.searchIndexes?.[doctype]
Expand All @@ -102,8 +107,8 @@ class SearchEngine {
}

handleDeletedDoc(doc: CozyDoc): void {
const doctype: string | undefined = doc._type
if (!doctype) {
const doctype = doc._type
if (!isSearchedDoctype(doctype)) {
return
}
const searchIndex = this.searchIndexes?.[doctype]
Expand All @@ -112,7 +117,7 @@ class SearchEngine {
return
}
log.debug('[REALTIME] remove doc from index after update : ', doc)
this.searchIndexes[doctype].index.remove(doc._id)
this.searchIndexes[doctype].index.remove(doc._id!)

this.debouncedReplication()
}
Expand All @@ -126,6 +131,7 @@ class SearchEngine {
const flexsearchIndex = new FlexSearch.Document<CozyDoc, true>({
tokenize: 'forward',
encode: getSearchEncoder(),
// @ts-ignore
minlength: 2,
document: {
id: '_id',
Expand All @@ -152,7 +158,7 @@ class SearchEngine {

shouldIndexDoc(doc: CozyDoc): boolean {
if (isIOCozyFile(doc)) {
const notInTrash = !doc.trashed && !/^\/\.cozy_trash/.test(doc.path)
const notInTrash = !doc.trashed && !/^\/\.cozy_trash/.test(doc.path ?? '')
const notRootDir = doc._id !== ROOT_DIR_ID
// Shared drives folder to be hidden in search.
// The files inside it though must appear. Thus only the file with the folder ID is filtered out.
Expand All @@ -163,7 +169,9 @@ class SearchEngine {
return true
}

async indexDocsForSearch(doctype: string): Promise<SearchIndex | null> {
async indexDocsForSearch(
doctype: keyof typeof SEARCH_SCHEMA
): Promise<SearchIndex | null> {
const searchIndex = this.searchIndexes[doctype]
const pouchLink = getPouchLink(this.client)

Expand All @@ -173,7 +181,9 @@ class SearchEngine {

if (!searchIndex) {
// First creation of search index
const docs = await this.client.queryAll(Q(doctype).limitBy(null))
const docs = await this.client.queryAll<CozyDoc[]>(
Q(doctype).limitBy(null)
)
const index = this.buildSearchIndex(doctype, docs)
const info = await pouchLink.getDbInfo(doctype)

Expand All @@ -199,7 +209,7 @@ class SearchEngine {
if (change.deleted) {
searchIndex.index.remove(change.id)
} else {
const normalizedDoc = { ...change.doc, _type: doctype }
const normalizedDoc = { ...change.doc, _type: doctype } as CozyDoc
this.addDocToIndex(searchIndex.index, normalizedDoc)
}
}
Expand All @@ -222,10 +232,19 @@ class SearchEngine {
const appsIndex = this.buildSearchIndex('io.cozy.apps', apps)

log.debug('Finished initializing indexes')
const currentDate = new Date().toISOString()
this.searchIndexes = {
[FILES_DOCTYPE]: { index: filesIndex, lastSeq: 0 },
[CONTACTS_DOCTYPE]: { index: contactsIndex, lastSeq: 0 },
[APPS_DOCTYPE]: { index: appsIndex, lastSeq: 0 }
[FILES_DOCTYPE]: {
index: filesIndex,
lastSeq: 0,
lastUpdated: currentDate
},
[CONTACTS_DOCTYPE]: {
index: contactsIndex,
lastSeq: 0,
lastUpdated: currentDate
},
[APPS_DOCTYPE]: { index: appsIndex, lastSeq: 0, lastUpdated: currentDate }
}
return this.searchIndexes
}
Expand All @@ -250,21 +269,25 @@ class SearchEngine {

searchOnIndexes(query: string): FlexSearchResultWithDoctype[] {
let searchResults: FlexSearchResultWithDoctype[] = []
for (const doctype in this.searchIndexes) {
for (const key in this.searchIndexes) {
const doctype = key as SearchedDoctype // XXX - Should not be necessary
const index = this.searchIndexes[doctype]
if (!index) {
log.warn('[SEARCH] No search index available for ', doctype)
continue
}
// TODO: do not use flexsearch store and rely on pouch storage?
// It's better for memory, but might slow down search queries
const indexResults = index.index.search(query, {
// XXX - The limit is specified twice because of a flexsearch inconstency
// that does not enforce the limit if only given in second argument, and
// does not return the correct type is only given in third options
const indexResults = index.index.search(query, LIMIT_DOCTYPE_SEARCH, {
limit: LIMIT_DOCTYPE_SEARCH,
enrich: true
})
const newResults = indexResults.map(res => ({
...res,
doctype
doctype: doctype
}))
searchResults = searchResults.concat(newResults)
}
Expand All @@ -278,11 +301,14 @@ class SearchEngine {
item.result.map(r => ({ ...r, field: item.field, doctype: item.doctype }))
)

const resultMap = new Map<FlexSearch.Id[], any>()
type MapItem = Omit<(typeof combinedResults)[number], 'field'> & {
fields: string[]
}
const resultMap = new Map<FlexSearch.Id[], MapItem>()

combinedResults.forEach(({ id, field, ...rest }) => {
if (resultMap.has(id)) {
resultMap.get(id).fields.push(field)
resultMap.get(id)?.fields.push(field)
} else {
resultMap.set(id, { id, fields: [field], ...rest })
}
Expand Down
9 changes: 8 additions & 1 deletion src/search/consts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const SEARCHABLE_DOCTYPES = [
'io.cozy.files',
'io.cozy.contacts',
'io.cozy.apps'
] as const
export type SearchedDoctype = (typeof SEARCHABLE_DOCTYPES)[number]

// Attribute order matters to apply priority on matching results
export const SEARCH_SCHEMA = {
export const SEARCH_SCHEMA: Record<SearchedDoctype, string[]> = {
'io.cozy.files': ['name', 'path'],
'io.cozy.contacts': [
'displayName',
Expand Down
6 changes: 5 additions & 1 deletion src/search/helpers/replication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ jest.mock('@/search/helpers/client', () => ({
getPouchLink: jest.fn()
}))

interface PouchLink {
startReplication: Function
}

describe('startReplicationWithDebounce', () => {
let client: CozyClient
let pouchLink: any
let pouchLink: PouchLink

beforeEach(() => {
client = new CozyClient()
Expand Down
24 changes: 14 additions & 10 deletions src/search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import FlexSearch from 'flexsearch'

import { IOCozyFile, IOCozyContact, IOCozyApp } from 'cozy-client/types/types'

import { APPS_DOCTYPE, CONTACTS_DOCTYPE, FILES_DOCTYPE } from '@/search/consts'
import {
APPS_DOCTYPE,
CONTACTS_DOCTYPE,
FILES_DOCTYPE,
SEARCH_SCHEMA,
SearchedDoctype
} from '@/search/consts'

export type CozyDoc = IOCozyFile | IOCozyContact | IOCozyApp

Expand All @@ -18,17 +24,15 @@ export const isIOCozyApp = (doc: CozyDoc): doc is IOCozyApp => {
return doc._type === APPS_DOCTYPE
}

const searchedDoctypes = [
APPS_DOCTYPE,
CONTACTS_DOCTYPE,
FILES_DOCTYPE
] as const
export type SearchedDoctype = (typeof searchedDoctypes)[number]
const searchedDoctypes = Object.keys(SEARCH_SCHEMA)

export const isSearchedDoctype = (
doctype: string
doctype: string | undefined
): doctype is SearchedDoctype => {
return true
if (!doctype) {
return false
}
return searchedDoctypes.includes(doctype)
}

export interface RawSearchResult
Expand All @@ -52,5 +56,5 @@ export interface SearchIndex {
}

export type SearchIndexes = {
[key: string]: SearchIndex
[key in SearchedDoctype]: SearchIndex
}
Loading