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
- Setup with cellx
- Rollback on promise error
- Update on promise success
- Attach all synced updates to last running fetch
- Init a, b, c
- Update a, b, add fetch a to queue, run fetch a
- Update b, attach to current fetch a
- Update c, add fetch c to queue
- Update b, add fetch b to queue
- fetch a complete, commit a, run fetch c
- fetch c error ask user to retry/abort
- on retry run fetch c, get error and ask again
- on abort cancel all queue, rollback c to state in 1, b to state in 3
See complex example
// @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})
})
// @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
*/
// @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 }
*/
// @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
*/