@worldbrain/storex-sync
Version:
Offline-first syncing between multiple databases using Storex
178 lines (147 loc) • 9.19 kB
text/typescript
import { StorageRegistry } from '@worldbrain/storex';
import * as expect from 'expect'
import { ClientSyncLogEntry } from '../client-sync-log/types';
import { ExecutableOperation } from './types';
import { reconcileSyncLog } from './default';
function test({logEntries, expectedOperations} : {logEntries : ClientSyncLogEntry[], expectedOperations? : ExecutableOperation[]}) {
const storageRegistry = new StorageRegistry()
storageRegistry.registerCollections({
list: {
version: new Date('2019-02-02'),
fields: {
title: {type: 'string'},
}
},
listEntry: {
version: new Date('2019-02-02'),
fields: {
title: {type: 'string'},
url: {type: 'string'},
}
}
})
const reconciled = reconcileSyncLog(logEntries, {storageRegistry})
if (expectedOperations) {
expect(reconciled).toEqual(expectedOperations)
}
}
describe('Reconciliation', () => {
it('should choose the newest write when finding two entries for the same object field', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'first'},
]
test({logEntries, expectedOperations: [
{operation: 'updateOneObject', collection: 'list', args: [{id: 'list-one'}, {title: 'second'}]}
]})
})
it('should choose the newest write when finding more than two entries for the same object field', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'modify', createdOn: 3, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'third'},
{operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'first'},
]
test({logEntries, expectedOperations: [
{operation: 'updateOneObject', collection: 'list', args: [{id: 'list-one'}, {title: 'third'}]}
]})
})
it('should ignore writes to an object that needs deletion', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'delete', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'},
]
test({logEntries, expectedOperations: [
{operation: 'deleteOneObject', collection: 'list', args: [{id: 'list-one'}]}
]})
})
it('should ignore writes to an already deleted object', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'delete', createdOn: 4, sharedOn: 3, needsIntegration: true, collection: 'list', pk: 'list-one'},
{operation: 'delete', createdOn: 1, sharedOn: 3, needsIntegration: true, collection: 'list', pk: 'list-one'},
]
test({logEntries, expectedOperations: []})
})
it('should work with only one delete', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'delete', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'},
]
test({logEntries, expectedOperations: [
{operation: 'deleteOneObject', collection: 'list', args: [{id: 'list-one'}]}
]})
})
it('should ignore double deletes', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'delete', createdOn: 4, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'},
{operation: 'delete', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'},
]
test({logEntries, expectedOperations: [
{operation: 'deleteOneObject', collection: 'list', args: [{id: 'list-one'}]}
]})
})
it('should work with deletes having compound keys', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'delete', createdOn: 4, sharedOn: null, needsIntegration: true, collection: 'listEntry', pk: ['list-one', 3]},
]
test({logEntries, expectedOperations: [
{operation: 'deleteOneObject', collection: 'listEntry', args: [{id: ['list-one', 3]}]}
]})
})
it('should ignore writes that are already synced', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'modify', createdOn: 2, sharedOn: 3, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
]
test({logEntries, expectedOperations: []})
})
it('should create objects', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'create', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {title: 'first'}}
]
test({logEntries, expectedOperations: [
{operation: 'createObject', collection: 'list', args: {id: 'list-one', title: 'first'}}
]})
})
it('should consolidate object creation with object updates', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'create', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {title: 'first', prio: 5}},
]
test({logEntries, expectedOperations: [
{operation: 'createObject', collection: 'list', args: {id: 'list-one', title: 'second', prio: 5}}
]})
})
it('should consolidate object creation with object deletion', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'create', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}},
{operation: 'delete', createdOn: 3, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'},
]
test({logEntries, expectedOperations: []})
})
it('should complain about double creates', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'create', createdOn: 1, sharedOn: 1, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}},
{operation: 'create', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}},
]
expect(() => test({logEntries})).toThrow(`Detected double create in collection 'list', pk '"list-one"'`)
})
it('should complain about modifications made to an object before creation', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
{operation: 'create', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}},
]
expect(() => test({logEntries})).toThrow(
`Detected modification to collection 'list', pk '"list-one"' before it was created (likely pk collision)`
)
})
it('should complain about modifications made to an object before creation even if received in the right order', () => {
const logEntries : ClientSyncLogEntry[] = [
{operation: 'create', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}},
{operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'},
]
expect(() => test({logEntries})).toThrow(
`Detected modification to collection 'list', pk '"list-one"' before it was created (likely pk collision)`
)
})
})