UNPKG

@nozbe/watermelondb

Version:

Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast

151 lines (127 loc) 4.93 kB
// @flow import { values } from '../../utils/fp' import areRecordsEqual from '../../utils/fp/areRecordsEqual' 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' // 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 // and will be applied on top of the remote version. export function resolveConflict(local: RawRecord, remote: DirtyRaw): DirtyRaw { // We SHOULD NOT have a reference to a `deleted` record, but since it was locally // deleted, there's nothing to update, since the local deletion will still be pushed to the server -- return raw as is if (local._status === 'deleted') { return local } // mutating code - performance-critical path const resolved = { // use local fields if remote is missing columns (shouldn't but just in case) ...local, // Note: remote MUST NOT have a _status of _changed fields (will replace them anyway just in case) ...remote, id: local.id, _status: local._status, _changed: local._changed, } // Use local properties where changed local._changed.split(',').forEach((column) => { resolved[column] = local[column] }) return resolved } function replaceRaw(record: Model, dirtyRaw: DirtyRaw): void { record._raw = sanitizedRaw(dirtyRaw, record.collection.schema) } export function prepareCreateFromRaw<T: Model>(collection: Collection<T>, dirtyRaw: DirtyRaw): T { // TODO: Think more deeply about this - it's probably unnecessary to do this check, since it would // mean malicious sync server, which is a bigger problem invariant( // $FlowFixMe !Object.prototype.hasOwnProperty.call(dirtyRaw, '__proto__'), 'Malicious dirtyRaw detected - contains a __proto__ key', ) const raw = Object.assign({}, dirtyRaw, { _status: 'synced', _changed: '' }) // faster than object spread return collection.prepareCreateFromDirtyRaw(raw) } // optimization - don't run DB update if received record is the same as local // (this happens a lot during replacement sync) export function requiresUpdate<T: Model>( collection: Collection<T>, local: RawRecord, dirtyRemote: DirtyRaw, ): boolean { if (local._status !== 'synced') { return true } const remote = sanitizedRaw(dirtyRemote, collection.schema) remote._status = 'synced' const canSkipSafely = areRecordsEqual(local, remote) return !canSkipSafely } export const recordFromRaw = <T: Model>(raw: RawRecord, collection: Collection<T>): T => collection._cache._modelForRaw(raw, false) export function prepareUpdateFromRaw<T: Model>( localRaw: RawRecord, remoteDirtyRaw: DirtyRaw, collection: Collection<T>, log: ?SyncLog, conflictResolver?: SyncConflictResolver, ): ?T { if (!requiresUpdate(collection, localRaw, remoteDirtyRaw)) { return null } const local = recordFromRaw(localRaw, collection) // Note COPY for log - only if needed const logConflict = log && !!localRaw._changed const logLocal = logConflict ? { // $FlowFixMe ...localRaw, } : {} const logRemote = logConflict ? { ...remoteDirtyRaw } : {} let newRaw = resolveConflict(localRaw, remoteDirtyRaw) if (conflictResolver) { newRaw = conflictResolver(collection.table, localRaw, remoteDirtyRaw, newRaw) } // $FlowFixMe return local.prepareUpdate(() => { replaceRaw(local, newRaw) // log resolved conflict - if any if (logConflict && log) { log.resolvedConflicts = log.resolvedConflicts || [] log.resolvedConflicts.push({ local: logLocal, remote: logRemote, // $FlowFixMe resolved: { ...newRaw }, }) } }) } export function prepareMarkAsSynced<T: Model>(record: T): T { // $FlowFixMe const newRaw = Object.assign({}, record._raw, { _status: 'synced', _changed: '' }) // faster than object spread // $FlowFixMe return record.prepareUpdate(() => { replaceRaw(record, newRaw) }) } export function ensureSameDatabase(database: Database, initialResetCount: number): void { invariant( database._resetCount === initialResetCount, `[Sync] Sync aborted because database was reset`, ) } export const isChangeSetEmpty: (SyncDatabaseChangeSet) => boolean = (changeset) => values(changeset).every( ({ created, updated, deleted }) => created.length + updated.length + deleted.length === 0, ) const sum: (number[]) => number = (xs) => xs.reduce((a, b) => a + b, 0) export const changeSetCount: (SyncDatabaseChangeSet) => number = (changeset) => sum( values(changeset).map( ({ created, updated, deleted }) => created.length + updated.length + deleted.length, ), )