Skip to content

Commit

Permalink
fix: Realtime conflict into Profile page
Browse files Browse the repository at this point in the history
The document sent by cozy-stack in real time is the CouchDB document. Unlike a call to the JSON Api, the document does not contain the attribute field. When a change was made, it generated a conflict error. I modified the RealTimeQueries so that I could modify the real-time object before integrating it into the cozy-client. This makes it possible to have the same version everywhere and avoid conflicts.

See also: cozy/cozy-client#1412
  • Loading branch information
cballevre committed Oct 30, 2023
1 parent 34deba8 commit bf7569f
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { initFlags } from 'lib/flags'
import { routes } from 'constants/routes'
import ChangeEmail from 'components/Email/ChangeEmail'
import { Subscription } from 'components/Subscription/Subscription'
import SettingsRealTimeQueries from 'components/SettingsRealTimeQueries'

initFlags()

Expand All @@ -55,7 +56,7 @@ export const App = () => {
<Alerter />
{isBigView && <Sidebar />}
<RealTimeQueries doctype="io.cozy.oauth.clients" />
<RealTimeQueries doctype="io.cozy.settings" />
<SettingsRealTimeQueries />
<Main>
<Routes>
{isSmallView && <Route path="/menu" element={<Menu />} />}
Expand Down
146 changes: 146 additions & 0 deletions src/components/SettingsRealTimeQueries.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { memo, useEffect } from 'react'
import { useClient, Mutations } from 'cozy-client'
import { receiveMutationResult } from 'cozy-client/dist/store'

/**
* Normalizes an object representing a CouchDB document
*
* Ensures existence of `_type`
*
* @public
* @param {CouchDBDocument} couchDBDoc - object representing the document
* @returns {CozyClientDocument} full normalized document
*/
const normalizeDoc = (couchDBDoc, doctype) => {
return {
id: couchDBDoc._id,
_type: doctype,
...couchDBDoc
}
}

/**
* DispatchChange
*
* @param {CozyClient} client CozyClient instane
* @param {Doctype} doctype Doctype of the document to update
* @param {CouchDBDocument} couchDBDoc Document to update
* @param {Mutation} mutationDefinitionCreator Mutation to apply
*/
const dispatchChange = (
client,
doctype,
couchDBDoc,
mutationDefinitionCreator
) => {
const data = normalizeDoc(couchDBDoc, doctype)
const response = {
data
}

const options = {}
client.dispatch(
receiveMutationResult(
client.generateRandomId(),
response,
options,
mutationDefinitionCreator(data)
)
)
}

/**
* The document real time comes without attributes, there are only at the root.
* That's why we need to merge the attributes from the document in the store.
* @param {CozyClient} client - CozyClient instane
* @param {CouchDBDocument} docFromRealTime - object representing the document from real time
* @returns {object} merged document
*/
export const computeDocumentFromRealTime = (client, docFromRealTime) => {
const { _id, _type, _rev, ...attributes } = docFromRealTime
const docFromState = client.getDocumentFromState('io.cozy.settings', _id)

if (docFromState) {
return {
...docFromState,
_id,
_type,
_rev,
...attributes,
attributes: {
...docFromState?.attributes,
...attributes
},
meta: {
rev: _rev
}
}
}

return {
...docFromRealTime,
attributes: {
...attributes
}
}
}

/**
* Component that subscribes to io.cozy.settings document changes and keep the
* internal store updated. This is a copy of RealTimeQueries from cozy-client
* with a tweak to merge the changes with the existing document from the store.
* You can have more detail on the problematic we are solving here:
* https://github.com/cozy/cozy-client/issues/1412
*
* @param {object} options - Options
* @param {Doctype} options.doctype - The doctype to watch
* @returns {null} The component does not display anything.
*/
const SettingsRealTimeQueries = ({ doctype = 'io.cozy.settings' }) => {
const client = useClient()

useEffect(() => {
const realtime = client.plugins.realtime

if (!realtime) {
throw new Error(
'You must include the realtime plugin to use RealTimeQueries'
)
}

const dispatchCreate = couchDBDoc => {
const doc = computeDocumentFromRealTime(client, couchDBDoc)
dispatchChange(client, doctype, doc, Mutations.createDocument)
}
const dispatchUpdate = couchDBDoc => {
const doc = computeDocumentFromRealTime(client, couchDBDoc)
dispatchChange(client, doctype, doc, Mutations.updateDocument)
}
const dispatchDelete = couchDBDoc => {
const doc = computeDocumentFromRealTime(client, couchDBDoc)
dispatchChange(
client,
doctype,
{ ...doc, _deleted: true },
Mutations.deleteDocument
)
}

const subscribe = async () => {
await realtime.subscribe('created', doctype, dispatchCreate)
await realtime.subscribe('updated', doctype, dispatchUpdate)
await realtime.subscribe('deleted', doctype, dispatchDelete)
}
subscribe()

return () => {
realtime.unsubscribe('created', doctype, dispatchCreate)
realtime.unsubscribe('updated', doctype, dispatchUpdate)
realtime.unsubscribe('deleted', doctype, dispatchDelete)
}
}, [client, doctype])

return null
}

export default memo(SettingsRealTimeQueries)
76 changes: 76 additions & 0 deletions src/components/SettingsRealTimeQueries.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { computeDocumentFromRealTime } from 'components/SettingsRealTimeQueries'

describe('computeDocumentFromRealTime', () => {
const client = {
getDocumentFromState: jest.fn()
}

beforeEach(() => {
jest.clearAllMocks()
})

it('should merge attributes from real time document with existing document in store', () => {
const docFromRealTime = {
_id: 'io.cozy.settings.instance',
_type: 'io.cozy.settings',
_rev: 'rev-2',
public_name: 'Alice2'
}
const docFromState = {
_id: 'io.cozy.settings.instance',
_type: 'io.cozy.settings',
_rev: 'rev-1',
attributes: {
public_name: 'Alice',
password_defined: true
}
}
client.getDocumentFromState.mockReturnValueOnce(docFromState)

const result = computeDocumentFromRealTime(client, docFromRealTime)

expect(result).toEqual({
_id: 'io.cozy.settings.instance',
_type: 'io.cozy.settings',
_rev: 'rev-2',
public_name: 'Alice2',
attributes: {
public_name: 'Alice2',
password_defined: true
},
meta: {
rev: 'rev-2'
}
})
expect(client.getDocumentFromState).toHaveBeenCalledWith(
'io.cozy.settings',
'io.cozy.settings.instance'
)
})

it('should add attributes from real time document if no existing document in store', () => {
const docFromRealTime = {
_id: 'io.cozy.settings.instance',
_type: 'io.cozy.settings',
_rev: 'rev-1',
public_name: 'Alice'
}
client.getDocumentFromState.mockReturnValueOnce(null)

const result = computeDocumentFromRealTime(client, docFromRealTime)

expect(result).toEqual({
_id: 'io.cozy.settings.instance',
_type: 'io.cozy.settings',
_rev: 'rev-1',
public_name: 'Alice',
attributes: {
public_name: 'Alice'
}
})
expect(client.getDocumentFromState).toHaveBeenCalledWith(
'io.cozy.settings',
'io.cozy.settings.instance'
)
})
})

0 comments on commit bf7569f

Please sign in to comment.