Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions docs/api/cozy-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ Retrieve intance info like context, uuid, disk usage etc

*Defined in*

[packages/cozy-client/src/hooks/useQuery.js:93](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L93)
[packages/cozy-client/src/hooks/useQuery.js:94](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L94)

***

Expand All @@ -999,7 +999,7 @@ Fetches a queryDefinition and returns the queryState

*Defined in*

[packages/cozy-client/src/hooks/useQuery.js:28](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L28)
[packages/cozy-client/src/hooks/useQuery.js:29](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L29)

***

Expand Down
1 change: 1 addition & 0 deletions docs/api/cozy-client/classes/CozyClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
| `autoHydrate` | `boolean` | - |
| `backgroundFetching` | `boolean` | If set to true, backgroundFetching will be enabled by default on every query. Meaning that, when the fetchStatus has already been loaded, it won't be updated during future fetches. Instead, a `isFetching` attribute will be used to indicate when background fetching is started. |
| `client` | `any` | - |
| `forceHydratation` | `boolean` | If set to true, all documents will be hydrated w.r.t. the provided schema's relationships, even if the relationship does not exist on the doc. |
| `oauth` | `any` | - |
| `onError` | `Function` | Default callback if a query is errored |
| `onTokenRefresh` | `Function` | - |
Expand Down
44 changes: 37 additions & 7 deletions packages/cozy-client/src/CozyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const DOC_UPDATE = 'update'
* @property {import("./types").ClientCapabilities} [capabilities] - Capabilities sent by the stack
* @property {boolean} [store] - If set to false, the client will not instantiate a Redux store automatically. Use this if you want to merge cozy-client's store with your own redux store. See [here](https://docs.cozy.io/en/cozy-client/react-integration/#1b-use-your-own-redux-store) for more information.
* @property {import('./performances/types').PerformanceAPI} [performanceApi] - The performance API that can be used to measure performances
* @property {boolean} [forceHydratation] - If set to true, all documents will be hydrated w.r.t. the provided schema's relationships, even if the relationship does not exist on the doc.
*/

/**
Expand Down Expand Up @@ -1156,7 +1157,18 @@ client.query(Q('io.cozy.bills'))`)
if (!Array.isArray(data)) {
await this.persistVirtualDocument(data, enforce)
} else {
for (const document of data) {
const documentsToPersist = data.filter(document => {
if (!document || document.cozyLocalOnly) {
return false
}

if ((!document.meta?.rev && !document._rev) || enforce) {
return true
}

return false
})
for (const document of documentsToPersist) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really better? OK you don't call a method if not needed, but now you loop twice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually significantly better. After a big query retrieving 25K docs, the persistVirtualDocuments takes:

  • Before: 609ms
  • After: 20ms

I wonder if this is because of the event loop overwhelmed by too many calls

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh shit, I didn't saw that the persistVirtualDocument was async... This is why.

nit: You can now remove the check within the persistVirtualDocument method since should only call it with the docs to persist ^^.

await this.persistVirtualDocument(document, enforce)
}
}
Expand Down Expand Up @@ -1216,7 +1228,7 @@ client.query(Q('io.cozy.bills'))`)
if (queryDef instanceof QueryDefinition) {
definitions.push(queryDef)
} else {
documents.push(queryDef)
documents.push(doc)
}
} catch {
// eslint-disable-next-line
Expand Down Expand Up @@ -1332,9 +1344,11 @@ client.query(Q('io.cozy.bills'))`)

hydrateRelationships(document, schemaRelationships) {
const methods = this.getRelationshipStoreAccessors()
return mapValues(schemaRelationships, (assoc, name) =>
createAssociation(document, assoc, methods)
)
return mapValues(schemaRelationships, (assoc, name) => {
if (this.options?.forceHydratation || document.relationships?.[assoc]) {
return createAssociation(document, assoc, methods)
}
})
}

/**
Expand Down Expand Up @@ -1444,13 +1458,29 @@ client.query(Q('io.cozy.bills'))`)
return queryResults
}

const data =
const hydratedData =
hydrated && doctype
? this.hydrateDocuments(doctype, queryResults.data)
: queryResults.data

const relationships = this.schema.getDoctypeSchema(doctype)?.relationships
const relationshipNames = relationships
? Object.keys(relationships)
: null

// The `data` array contains the hydrated data with the relationships, if any.
// The `storeData` array contains the documents from the store: this is useful to preserve
// referential equality, to be later evaluated to determine whether or not the
// documents had changed.
return {
...queryResults,
data: isSingleDocQuery && singleDocData ? data[0] : data
data:
isSingleDocQuery && singleDocData ? hydratedData[0] : hydratedData,
storeData:
isSingleDocQuery && singleDocData
? queryResults.data[0]
: queryResults.data,
relationshipNames
}
} catch (e) {
logger.warn(
Expand Down
3 changes: 2 additions & 1 deletion packages/cozy-client/src/hooks/useQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useClient from './useClient'
import logger from '../logger'
import { clientContext } from '../context'
import { QueryDefinition } from '../queries/dsl'
import { equalityCheckForQuery } from './utils'

const useSelector = createSelectorHook(clientContext)

Expand Down Expand Up @@ -61,7 +62,7 @@ const useQuery = (queryDefinition, options) => {
hydrated: get(options, 'hydrated', true),
singleDocData: get(options, 'singleDocData', false)
})
})
}, equalityCheckForQuery)

useEffect(
() => {
Expand Down
107 changes: 107 additions & 0 deletions packages/cozy-client/src/hooks/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Equality check
*
* Note we do not make a shallow equality check on documents, as it is less efficient and should
* not be necessary: the queryResult.data is built by extracting documents from the state, thus
* preserving references.
*
* @param {import("../types").QueryStateResult} queryResA - A query result to compare
* @param {import("../types").QueryStateResult} queryResB - A query result to compare
* @returns
*/
export const equalityCheckForQuery = (queryResA, queryResB) => {
//console.log('Call equality check : ', queryResA, queryResB)
if (queryResA === queryResB) {
// Referential equality
return true
}

if (
typeof queryResA !== 'object' ||
queryResA === null ||
typeof queryResB !== 'object' ||
queryResB === null
) {
// queryResA or queryResB is not an object or null
return false
}

if (queryResA.id !== queryResB.id) {
return false
}
if (queryResA.fetchStatus !== queryResB.fetchStatus) {
return false
}

const docsA = queryResA.storeData
const docsB = queryResB.storeData
if (!docsA || !docsB) {
// No data to check
return false
}
if (!Array.isArray(docsA) && !Array.isArray(docsB) && docsA !== docsB) {
// Only one doc
return false
}

if (
Array.isArray(docsA) &&
Array.isArray(docsB) &&
!arraysHaveSameLength(docsA, docsB)
) {
// A document was added or removed
return false
}

if (Array.isArray(docsA) && Array.isArray(docsB)) {
for (let i = 0; i < docsA.length; i++) {
if (docsA[i] !== docsB[i]) {
// References should be the same for non-updated documents
return false
}
}
}

if (queryResA.relationshipNames) {
// In case of relationships, we cannot check referential equality, because we
// "hydrate" the data by creating a new instance of the related relationship class.
// Thus, we check the document revision instead.
const hydratedDataA = queryResA.data
const hydratedDataB = queryResB.data
if (!Array.isArray(hydratedDataA) && !Array.isArray(hydratedDataB)) {
// One doc with changed relationship
return revsAreEqual(hydratedDataA, hydratedDataB)
}
if (!arraysHaveSameLength(hydratedDataA, hydratedDataB)) {
// A relationship have been added or removed
return false
}
if (Array.isArray(hydratedDataA) && Array.isArray(hydratedDataB)) {
for (let i = 0; i < hydratedDataA.length; i++) {
for (const name of queryResA.relationshipNames) {
// Check hydrated relationship
const includedA = hydratedDataA[i][name]
const includedB = hydratedDataB[i][name]
if (includedA && includedB) {
if (!revsAreEqual(includedA, includedB)) {
return false
}
}
}
}
}
}
return true
}

const revsAreEqual = (docA, docB) => {
return docA?._rev === docB?._rev
}

const arraysHaveSameLength = (arrayA, arrayB) => {
return (
Array.isArray(arrayA) &&
Array.isArray(arrayB) &&
arrayA.length === arrayB.length
)
}
Loading