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

Synchronization option now supports syncUpdateCondition to skip remote record #1548

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

primus11
Copy link

@primus11 primus11 commented Feb 27, 2023

Atm synchronization only allows to customize conflict resolution which doesn't provide full control when trying for example to skip remote record altogether. It is possible to use conflict resolution that would return local but this is happening too late and has side effect of modifying updated_at.

edit - some more notes:

It is currently problematic to achieve idempotency when doing repetitive pullChanges. For example I would expect below code to be no-op but since potential local change will be resolved our updated_at will be updated. I am aware that custom "updated_at" could be used instead but that feels more like working against WatermelonDB.

function conflictResolver(
	tableName: TableName<any>,
	local: DirtyRaw,
	remote: DirtyRaw,
	resolved: DirtyRaw,
): DirtyRaw {
	const localUpdatedAt = local.updated_at;
	if (localUpdatedAt == null) {
		return remote;
	}

	const remoteUpdatedAt = remote.updated_at;

	if (localUpdatedAt > remoteUpdatedAt) {
		return local;
	} else {
		return remote;
	}
}

@radex
Copy link
Collaborator

radex commented Feb 27, 2023

Hi, this makes sense to me. I propose changing this to work more similarly to to conflictResolver - i.e. first the built-in requiresUpdate runs, then the result of that is passed to a custom shouldUpdateRecord? function. This way, it's going to be easier to use this correctly - override the default behavior just in specific cases, but leave the built-in optimization on otherwise.

For example I would expect below code to be no-op but since potential local change will be resolved our updated_at will be updated

BTW I'm not sure if this is correct. It's arguable whether this is expected behavior, but I think that's a bug. I specifically added doesn't touch created_at/updated_at when applying updates test

@primus11
Copy link
Author

primus11 commented Feb 28, 2023

I can move it yeah if that is preferred. My thinking at the end was that it maybe belongs to requiresUpdate and that if possible it is better that it is done before sanitizedRow and areRecordsEqual in it. Atm at least, it can also be before built-in requiresUpdate. Please just reconfirm and I will also change naming to shouldUpdateRecord at that point.

BTW I'm not sure if this is correct. It's arguable whether this is expected behavior, but I think that's a bug. I specifically added doesn't touch created_at/updated_at when applying updates test

This puzzled me a bit as well. The thing is that areRecordsEqual is triggered before conflictResolver takes place which automatically means that this record will go into prepareUpdate at that point. My first thinking was to add control when updated_at is updated since I believe it could be used elsewhere but it would be a bit funky from prepareUpdateFromRaw perspective and this change just achieves wanted result a lot better.

Edit: Maybe areRecordsEqual should be moved but it could potentially be worse performance-wise for some

@radex
Copy link
Collaborator

radex commented Feb 28, 2023

that if possible it is better that it is done before sanitizedRow and areRecordsEqual in it

Ok, rename it to shouldUpdateRecord, but keep the arguments as-is. We could export the original implementation (possibly renamed for consistency) so that one can use that.

description: 'remote', // remote change
project_id: 'orig', // unchanged
})
})
Copy link
Author

Choose a reason for hiding this comment

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

    it('forced local should keep record intact', async () => {
      const { database, comments } = makeDatabase()

      await makeLocalChanges(database)
      await testApplyRemoteChanges(database, {
        mock_comments: {
          updated: [{ id: 'cSynced', body: 'remote' }],
        },
      }, {
        conflictResolver: (
          tableName: TableName<any>,
          local: DirtyRaw,
          remote: DirtyRaw,
          resolved: DirtyRaw,
        ) => {
          return local;
        }
      })

      await expectSyncedAndMatches(comments, 'cSynced', {
        _status: 'synced',
        _changed: '',
      })
    })

will cause

 FAIL  src/sync/impl/__tests__/applyRemote.test.js (32.296 s)
   applyRemoteChanges  conflictResolver  forced local should keep record intact

    expect(received).toMatchObject(expected)

    - Expected  - 2
    + Received  + 2

      Object {
    -   "_changed": "",
    -   "_status": "synced",
    +   "_changed": "updated_at",
    +   "_status": "updated",

src/sync/impl/helpers.js Outdated Show resolved Hide resolved
Atm synchronization only allows to customize conflict resolution which doesn't provide full control when trying for example to skip remote record altogether. It is possible to use conflict resolution that would return local but this is happening too late and has side effect of modifying updated_at.
… conflictResolver where appropriate, added tests
@primus11 primus11 mentioned this pull request Jan 19, 2024
@primus11 primus11 mentioned this pull request Feb 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants