Skip to content

zerkalica/opti-update

Repository files navigation

opti-update

Framework-agnostic, low-cost optimistic updates with transactions and rollbacks.

What if in browser, todos in todomvc added asyncronously from server updates. How and when to rollback state changes on server error, how to handle multiple fetches in todomvc, when first fetch is running?

opti-update controls atom-values and fetch status. Can be used with any atom-like lirary: mobx, cellx, derivable, atmover etc.

// @flow
const updater = new AtomUpdater({
    transact: cellx.transact,
    abortOnError: true,
    rollback: true
})

const transaction = updater.transaction({
    fetcher: {
        fetch() {
            return Promise.reject(new Error('some'))
        }
    },
    setter: new GenericAtomSetter(atom, status)
})
    .set(a, '2')
    .set(b, '2')
    .run()

Example scenario

  1. Init a, b, c
  2. Update a, b, add fetch a to queue, run fetch a
  3. Update b, attach to current fetch a
  4. Update c, add fetch c to queue
  5. Update b, add fetch b to queue
  6. fetch a complete, commit a, run fetch c
  7. fetch c error ask user to retry/abort
  8. on retry run fetch c, get error and ask again
  9. on abort cancel all queue, rollback c to state in 1, b to state in 3

See complex example

opti-update flow diagram

Setup with cellx

// @flow

import cellx from 'cellx'
import {AtomUpdater, UpdaterStatus, RecoverableError} from 'opti-update'
import type {Atom, AtomUpdaterOpts} from 'opti-update'

const Cell = cellx.Cell
cellx.configure({asynchronous: false})

const updater = new AtomUpdater({
    transact: cellx.transact,
    abortOnError: true,
    rollback: true
})

const a = new Cell('1')
const b = new Cell('1')
const aStatus = new Cell(new UpdaterStatus('pending'))

a.subscribe((err: ?Error, {value}) => {
    console.log('a =', value)
})
b.subscribe((err: ?Error, {value}) => {
    console.log('b =', value)
})
aStatus.subscribe((err: ?Error, {value}) => {
    console.log('c =', {...value, error: value.error ? value.error.message : null})
})

Rollback on promise error

// @flow
//...
updater.transaction({
    setter: new GenericAtomSetter(a, aStatus),
    fetcher: {
        fetch() {
            return Promise.reject(new Error('some'))
        }
    }
})
    .set(a, '2')
    .set(b, '2')
    .run()

/*
c = { type: 'pending', complete: false, pending: true, error: null }
a = 2
b = 2
c = { type: 'error', complete: false, pending: false, error: 'some' }
b = 1
a = 1
 */

Update on promise success

// @flow
//...
updater.transaction({
    setter: new GenericAtomSetter(a, aStatus),
    fetcher: {
        fetch() {
            return Promise.resolve('3')
        }
    }
})
    .set(a, '2')
    .set(b, '2')
    .run()

/*
c = UpdaterStatus { type: 'pending', complete: false, pending: true, error: null }
a = 2
b = 2
a = 3
c = UpdaterStatus { type: 'complete', complete: true, pending: false, error: null }
*/

Attach all synced updates to last running fetch

// @flow
//...
updater.transaction({
    setter: new GenericAtomSetter(a, aStatus),
    fetcher: {
        fetch() {
            return Promise.reject(new Error('some'))
        }
    }
})
    .set(a, '2')
    .set(b, '2')
    .run()

updater.transaction()
    .set(b, '3')
    .run()
/*
c = { type: 'pending', complete: false, pending: true, error: null }
a = 2
b = 2
b = 3
c = { type: 'error', complete: false, pending: false, error: 'some' }
b = 1
a = 1
 */

About

Framework-agnostic, low-cost optimistic updates with transactions and rollbacks.

Resources

Stars

Watchers

Forks

Packages

No packages published