UNPKG

@worldbrain/storex-sync

Version:

Offline-first syncing between multiple databases using Storex

235 lines (207 loc) 10.2 kB
import * as expect from 'expect' import { setupStorexTest } from '@worldbrain/storex-pattern-modules/lib/index.tests' import { SharedSyncLogStorage } from './shared-sync-log/storex'; import { ClientSyncLogStorage } from './client-sync-log'; import { CustomAutoPkMiddleware } from './custom-auto-pk'; import { SyncLoggingMiddleware } from './logging-middleware'; import { shareLogEntries, receiveLogEntries, doSync } from '.'; import { reconcileSyncLog } from './reconciliation'; type PromiseContentType<T> = T extends Promise<infer U> ? U : T describe('Storex sync integration tests', () => { async function setupBackend(options) { return setupStorexTest<{sharedSyncLog : SharedSyncLogStorage}>({ dbName: 'backend', collections: {}, modules: { sharedSyncLog: ({storageManager}) => new SharedSyncLogStorage({storageManager}) } }) } function createGetNow(options : {start : number}) { let now = options.start return () => now++ } async function setupClient(options : {backend, clientName : string, getNow : () => number, pkGenerator : () => string}) { const { storageManager, modules } = await setupStorexTest<{clientSyncLog : ClientSyncLogStorage}>({ dbName: `client-${options.clientName}`, collections: { user: { version: new Date('2019-01-01'), fields: { displayName: { type: 'string' } } }, email: { version: new Date('2019-01-01'), fields: { address: { type: 'string' }, }, relationships: [ { childOf: 'user' } ] } }, modules: { clientSyncLog: ({storageManager}) => new ClientSyncLogStorage({storageManager}) } }) const includeCollections = ['user', 'email'] const pkMiddleware = new CustomAutoPkMiddleware({ pkGenerator: options.pkGenerator }) pkMiddleware.setup({ storageRegistry: storageManager.registry, collections: includeCollections }) const syncLoggingMiddleware = new SyncLoggingMiddleware({ storageManager, clientSyncLog: modules.clientSyncLog, includeCollections }) syncLoggingMiddleware._getNow = options.getNow storageManager.setMiddleware([ pkMiddleware, syncLoggingMiddleware ]) return { storageManager, modules, deviceId: null, objects: {} } } async function setupTest(options : {clients? : {name : string}[], getNow : () => number}) { let idsGenerated = 0 const pkGenerator = () => `id-${++idsGenerated}` const userId = 1 const backend = await setupBackend({}) const clients : {[name : string] : PromiseContentType<ReturnType<typeof setupClient>>} = {} for (const { name } of options.clients || []) { clients[name] = await setupClient({backend, clientName: name, getNow: options.getNow, pkGenerator}) clients[name].deviceId = await backend.modules.sharedSyncLog.createDeviceId({ userId, sharedUntil: 10 }) } return { backend, clients, userId } } describe('shareLogEntries()', () => { async function setupShareTest() { const { backend, clients, userId } = await setupTest({clients: [{ name: 'one' }, { name: 'two' } ], getNow: createGetNow({start: 2})}) await clients.one.storageManager.collection('user').createObject({displayName: 'Joe', emails: [{address: 'joe@doe.com'}]}) const share = (options : {now: number}) => shareLogEntries({ sharedSyncLog: backend.modules.sharedSyncLog, clientSyncLog: clients.one.modules.clientSyncLog, userId, deviceId: clients.one.deviceId, now: options.now }) return { backend, clients, userId, share } } it('should correctly share log entries', async () => { const { backend, clients, userId, share } = await setupShareTest() await share({now: 55}) expect(await backend.modules.sharedSyncLog.getUnsyncedEntries({deviceId: clients.two.deviceId})).toEqual([ { id: expect.anything(), userId, deviceId: clients.one.deviceId, createdOn: 2, sharedOn: 55, data: '{"operation":"create","collection":"user","pk":"id-1","field":null,"value":{"displayName":"Joe"}}', }, { id: expect.anything(), userId, deviceId: clients.one.deviceId, createdOn: 3, sharedOn: 55, data: '{"operation":"create","collection":"email","pk":"id-2","field":null,"value":{"address":"joe@doe.com"}}', }, ]) }) it('should not reshare entries that are already shared', async () => { const { backend, clients, share } = await setupShareTest() await share({now: 55}) const entries = await backend.modules.sharedSyncLog.getUnsyncedEntries({deviceId: clients.two.deviceId}) await share({now: 60}) expect(await backend.modules.sharedSyncLog.getUnsyncedEntries({deviceId: clients.two.deviceId})).toEqual(entries) }) }) describe('receiveLogEntries()', () => { async function setupReceiveTest() { const { backend, clients, userId } = await setupTest({clients: [{ name: 'one' }, { name: 'two' } ], getNow: createGetNow({start: 2})}) const receive = async (options : {now : number}) => { await receiveLogEntries({ clientSyncLog: clients.one.modules.clientSyncLog, sharedSyncLog: backend.modules.sharedSyncLog, deviceId: clients.one.deviceId, now: options.now, }) } return { backend, clients, userId, receive } } it('should correctly receive unsynced entries and write them to the local log marked as needing integration', async () => { const { backend, clients, userId, receive } = await setupReceiveTest() await clients.one.storageManager.collection('user').createObject({displayName: 'Bob'}) await backend.modules.sharedSyncLog.writeEntries([ { userId, deviceId: clients.one.deviceId, createdOn: 5, sharedOn: 55, data: '{"operation":"create","collection":"user","pk":"id-2","field":null,"value":{"displayName":"Joe"}}', }, { userId, deviceId: clients.one.deviceId, createdOn: 7, sharedOn: 55, data: '{"operation":"create","collection":"email","pk":"id-3","field":null,"value":{"address":"joe@doe.com"}}', }, ], { userId, deviceId: clients.one.deviceId }) await receive({now: 60}) expect(await clients.one.modules.clientSyncLog.getEntriesCreatedAfter(1)).toEqual([ { id: expect.anything(), createdOn: 2, sharedOn: null, needsIntegration: false, collection: 'user', pk: 'id-1', operation: 'create', value: { displayName: 'Bob' }, }, { id: expect.anything(), createdOn: 5, sharedOn: 60, needsIntegration: true, collection: 'user', pk: 'id-2', field: null, operation: 'create', value: { displayName: 'Joe' }, }, { id: expect.anything(), createdOn: 7, sharedOn: 60, needsIntegration: true, collection: 'email', pk: 'id-3', field: null, operation: 'create', value: { address: 'joe@doe.com' }, } ]) }) }) describe('doSync()', () => { async function setupSyncTest() { const { backend, clients, userId } = await setupTest({ clients: [{ name: 'one' }, { name: 'two' }], getNow: createGetNow({ start: 50 }) }) const sync = async (options : {clientName : string, now : number}) => { const client = clients[options.clientName] await doSync({ clientSyncLog: client.modules.clientSyncLog, sharedSyncLog: backend.modules.sharedSyncLog, storageManager: client.storageManager, reconciler: reconcileSyncLog, now: options.now, userId, deviceId: client.deviceId }) } return { clients, sync } } it('should work when pulling changes after being offline', async () => { const { clients, sync } = await setupSyncTest() const orig = (await clients.one.storageManager.collection('user').createObject({displayName: 'Joe', emails: [{address: 'joe@doe.com'}]})).object const { emails, ...user } = orig await sync({clientName: 'one', now: 55}) await sync({clientName: 'two', now: 60}) expect(await clients.two.storageManager.collection('user').findObject({id: user.id})).toEqual(user) expect(await clients.two.storageManager.collection('email').findObject({id: emails[0].id})).toEqual(emails[0]) }) }) })