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
Open
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
82 changes: 82 additions & 0 deletions src/sync/impl/__tests__/applyRemote.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,4 +506,86 @@ describe('applyRemoteChanges', () => {
// Record ID mock_tasks#tSynced was sent over the bridge, but it's not cached
await database.get('mock_tasks').find('tSynced')
})
describe('shouldUpdateRecord', () => {
it('can ignore record', async () => {
const { database, tasks } = makeDatabase()

await makeLocalChanges(database)
await testApplyRemoteChanges(database, {
mock_tasks: {
updated: [
// update / updated - will be ignored when any local change
{ id: 'tUpdated', name: 'remote', description: 'remote' },
],
},
}, {
shouldUpdateRecord: (_table, local, _remote) => {
return local._status !== 'updated'
},
})

await expectSyncedAndMatches(tasks, 'tUpdated', {
_status: 'updated',
_changed: 'name,position',
name: 'local', // local change preserved
position: 100,
description: 'orig', // orig should be preserved
project_id: 'orig', // unchanged
})
})
it('can still update', async () => {
const { database, tasks } = makeDatabase()

await makeLocalChanges(database)
await testApplyRemoteChanges(database, {
mock_tasks: {
updated: [
// update / updated - should not be ignored when local wasn't changed
{ id: 'tSynced', name: 'remote', description: 'remote' },
],
},
}, {
shouldUpdateRecord: (_table, local, _remote) => {
return local._status !== 'updated'
},
})

await expectSyncedAndMatches(tasks, 'tSynced', {
_status: 'synced',
name: 'remote', // remote change
description: 'remote', // remote change
})
})
})
describe('conflictResolver', () => {
it('can account for conflictResolver', async () => {
const { database, tasks } = makeDatabase()

await makeLocalChanges(database)
await testApplyRemoteChanges(database, {
mock_tasks: {
updated: [
// update / updated - resolve and update (description is concat of local/remote on change)
{ id: 'tUpdated', name: 'remote', description: 'remote' },
],
},
}, {
conflictResolver: (_table, local, remote, resolved) => {
if (local.name !== remote.name) {
resolved.name = `${remote.name} ${local.name}`
}
return resolved
},
})

await expectSyncedAndMatches(tasks, 'tUpdated', {
_status: 'updated',
_changed: 'name,position',
name: 'remote local', // concat of remote and local change
position: 100,
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",

})
})
8 changes: 7 additions & 1 deletion src/sync/impl/applyRemote.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { Database } from '../..'

import type { SyncDatabaseChangeSet, SyncLog, SyncConflictResolver } from '../index'
import type {
SyncDatabaseChangeSet,
SyncLog,
SyncShouldUpdateRecord,
SyncConflictResolver,
} from '../index'

export default function applyRemoteChanges(
remoteChanges: SyncDatabaseChangeSet,
opts: {
db: Database,
sendCreatedAsUpdated: boolean,
log?: SyncLog,
shouldUpdateRecord?: SyncShouldUpdateRecord,
conflictResolver?: SyncConflictResolver,
_unsafeBatchPerCollection?: boolean,
}
Expand Down
22 changes: 19 additions & 3 deletions src/sync/impl/applyRemote.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
SyncDatabaseChangeSet,
SyncLog,
SyncConflictResolver,
SyncShouldUpdateRecord,
SyncPullStrategy,
} from '../index'
import { prepareCreateFromRaw, prepareUpdateFromRaw, recordFromRaw } from './helpers'
Expand All @@ -32,6 +33,7 @@ type ApplyRemoteChangesContext = $Exact<{
strategy?: ?SyncPullStrategy,
sendCreatedAsUpdated?: boolean,
log?: SyncLog,
shouldUpdateRecord?: SyncShouldUpdateRecord,
conflictResolver?: SyncConflictResolver,
_unsafeBatchPerCollection?: boolean,
}>
Expand Down Expand Up @@ -263,7 +265,7 @@ function prepareApplyRemoteChangesToCollection<T: Model>(
collection: Collection<T>,
context: ApplyRemoteChangesContext,
): Array<?T> {
const { db, sendCreatedAsUpdated, log, conflictResolver } = context
const { db, sendCreatedAsUpdated, log, shouldUpdateRecord, conflictResolver } = context
const { table } = collection
const {
created,
Expand Down Expand Up @@ -292,7 +294,14 @@ function prepareApplyRemoteChangesToCollection<T: Model>(
`[Sync] Server wants client to create record ${table}#${raw.id}, but it already exists locally. This may suggest last sync partially executed, and then failed; or it could be a serious bug. Will update existing record instead.`,
)
recordsToBatch.push(
prepareUpdateFromRaw(currentRecord, raw, collection, log, conflictResolver),
prepareUpdateFromRaw(
currentRecord,
raw,
collection,
log,
shouldUpdateRecord,
conflictResolver,
),
)
} else if (locallyDeletedIds.includes(raw.id)) {
logError(
Expand All @@ -312,7 +321,14 @@ function prepareApplyRemoteChangesToCollection<T: Model>(

if (currentRecord) {
recordsToBatch.push(
prepareUpdateFromRaw(currentRecord, raw, collection, log, conflictResolver),
prepareUpdateFromRaw(
currentRecord,
raw,
collection,
log,
shouldUpdateRecord,
conflictResolver,
),
)
} else if (locallyDeletedIds.includes(raw.id)) {
// Nothing to do, record was locally deleted, deletion will be pushed later
Expand Down
8 changes: 7 additions & 1 deletion src/sync/impl/helpers.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Model, Collection, Database } from '../..'
import type { RawRecord, DirtyRaw } from '../../RawRecord'
import type { SyncLog, SyncDatabaseChangeSet, SyncConflictResolver } from '../index'
import type {
SyncLog,
SyncDatabaseChangeSet,
SyncShouldUpdateRecord,
SyncConflictResolver,
} from '../index'

// Returns raw record with naive solution to a conflict based on local `_changed` field
// This is a per-column resolution algorithm. All columns that were changed locally win
Expand All @@ -16,6 +21,7 @@ export function prepareUpdateFromRaw<T extends Model = Model>(
record: T,
updatedDirtyRaw: DirtyRaw,
log?: SyncLog,
shouldUpdateRecord?: SyncShouldUpdateRecord,
conflictResolver?: SyncConflictResolver,
): T

Expand Down
17 changes: 15 additions & 2 deletions src/sync/impl/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { invariant } from '../../utils/common'

import type { Model, Collection, Database } from '../..'
import { type RawRecord, type DirtyRaw, sanitizedRaw } from '../../RawRecord'
import type { SyncLog, SyncDatabaseChangeSet, SyncConflictResolver } from '../index'
import type {
SyncLog,
SyncDatabaseChangeSet,
SyncShouldUpdateRecord,
SyncConflictResolver,
} from '../index'

// Returns raw record with naive solution to a conflict based on local `_changed` field
// This is a per-column resolution algorithm. All columns that were changed locally win
Expand Down Expand Up @@ -59,7 +64,14 @@ export function requiresUpdate<T: Model>(
collection: Collection<T>,
local: RawRecord,
dirtyRemote: DirtyRaw,
shouldUpdateRecord?: SyncShouldUpdateRecord,
): boolean {
if (shouldUpdateRecord) {
if (!shouldUpdateRecord(collection.table, local, dirtyRemote)) {
return false
}
}

if (local._status !== 'synced') {
return true
}
Expand All @@ -79,9 +91,10 @@ export function prepareUpdateFromRaw<T: Model>(
remoteDirtyRaw: DirtyRaw,
collection: Collection<T>,
log: ?SyncLog,
shouldUpdateRecord?: SyncShouldUpdateRecord,
conflictResolver?: SyncConflictResolver,
): ?T {
if (!requiresUpdate(collection, localRaw, remoteDirtyRaw)) {
if (!requiresUpdate(collection, localRaw, remoteDirtyRaw, shouldUpdateRecord)) {
return null
}

Expand Down
1 change: 1 addition & 0 deletions src/sync/impl/synchronize.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default function synchronize({
sendCreatedAsUpdated,
migrationsEnabledAtVersion,
log,
shouldUpdateRecord,
conflictResolver,
_unsafeBatchPerCollection,
unsafeTurbo,
Expand Down
2 changes: 2 additions & 0 deletions src/sync/impl/synchronize.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default async function synchronize({
sendCreatedAsUpdated = false,
migrationsEnabledAtVersion,
log,
shouldUpdateRecord,
conflictResolver,
_unsafeBatchPerCollection,
unsafeTurbo,
Expand Down Expand Up @@ -105,6 +106,7 @@ export default async function synchronize({
strategy: ((pullResult: any).experimentalStrategy: ?SyncPullStrategy),
sendCreatedAsUpdated,
log,
shouldUpdateRecord,
conflictResolver,
_unsafeBatchPerCollection,
})
Expand Down
9 changes: 9 additions & 0 deletions src/sync/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export type SyncLog = {
error?: Error;
}

export type SyncShouldUpdateRecord = (
table: TableName<any>,
local: DirtyRaw,
remote: DirtyRaw,
) => boolean

export type SyncConflictResolver = (
table: TableName<any>,
local: DirtyRaw,
Expand All @@ -64,6 +70,9 @@ export type SyncArgs = $Exact<{
migrationsEnabledAtVersion?: SchemaVersion;
sendCreatedAsUpdated?: boolean;
log?: SyncLog;
// Advanced (unsafe) customization point. Useful when doing per record conflict resolution and can
// determine directly from remote and local if we can keep local.
shouldUpdateRecord?: SyncShouldUpdateRecord;
// Advanced (unsafe) customization point. Useful when you have subtle invariants between multiple
// columns and want to have them updated consistently, or to implement partial sync
// It's called for every record being updated locally, so be sure that this function is FAST.
Expand Down
9 changes: 9 additions & 0 deletions src/sync/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export type SyncLog = {
error?: Error,
}

export type SyncShouldUpdateRecord = (
table: TableName<any>,
local: DirtyRaw,
remote: DirtyRaw,
) => boolean

export type SyncConflictResolver = (
table: TableName<any>,
local: DirtyRaw,
Expand All @@ -91,6 +97,9 @@ export type SyncArgs = $Exact<{
migrationsEnabledAtVersion?: SchemaVersion,
sendCreatedAsUpdated?: boolean,
log?: SyncLog,
// Advanced (unsafe) customization point. Useful when doing per record conflict resolution and can
// determine directly from remote and local if we can keep local.
shouldUpdateRecord?: SyncShouldUpdateRecord,
// Advanced (unsafe) customization point. Useful when you have subtle invariants between multiple
// columns and want to have them updated consistently, or to implement partial sync
// It's called for every record being updated locally, so be sure that this function is FAST.
Expand Down