Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Realtime conflict into Profile page #641

Merged
merged 1 commit into from
Oct 31, 2023
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
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'
)
})
})