// https://github.com/alexvcasillas/react-mobx-state-tree/blob/master/src/utils/mobx-state-tree.js
// --- util
const isError = x => Object.prototype.toString.call(x).includes('Error')
const isPromise = x => Object.prototype.toString.call(x).includes('Promise')
const isArray = Array.isArray
const isNill = x => x === null || x === undefined
const isReal = x => !isNill(x) && !Number.isNaN(x)
const flatten = x => [].concat.apply([], x)
const hasLength = x => x && isArray(x) && x.length !== 0
const NO_OP = () => {}
// --- logic ---
function wrap(fn, ...args) {
// @NOTE uses scoped `fn` here
// return function to call with arguments, unless we pass in arguments
function promiseFactory() {
// setup easy functions to make the promis sexier
let response = []
const update = (error = null, res = null) => {
response = flatten([error, res])
// fix when we have 3 entries & second is error
// this usually means promise rejected in a .then
if (isError(response[1])) response[0] = response[1]
return Promise.resolve(response)
}
const updateSuccess = res => update(null, res)
const updateError = update
// @NOTE important that try catch is inside of the promise
return new Promise((resolve, reject) => {
try {
const result = fn.apply(this, arguments)
return resolve(result)
} catch (error) {
return reject(error)
}
})
.catch(error => updateError(error))
.then(res => updateSuccess(res))
}
if (isReal(args) && hasLength(args)) return promiseFactory.apply(this, args)
else return promiseFactory
}
// --- lib
function defaultOnError(error) {
console.error(error)
this.state = 'error'
this.error = error
}
// @NOTE onCall is the fn
const defaultOnDone = result => {
console.debug('done', result)
this.state = 'done'
this.result = result
return result
}
function defaultOnPending() {
console.info('pending')
this.state = 'pending'
}
const defaultConfig = {
onError: defaultOnError,
onPending: defaultOnPending,
onDone: defaultOnDone,
}
const GeneratorProtocolPrototype = {
state: 'error|done|pending',
value: undefined,
done: false,
// index: 0,
// errored: false,
// next: NO_OP,
}
function toGenerator(fn, config = defaultConfig) {
// @NOTE no need for state, just `onReturn`
const state = isNill(config.state) ? {} : config.state
// setup
const generator = Object.create(GeneratorProtocolPrototype)
const wrappedFn = wrap(fn.bind(null, state))
// callbacks / events / lifecycle
const onDone = config.onDone || defaultOnDone
const onPending = config.onPending || defaultOnPending
const onError = config.onError || defaultOnError
return function* asyncAsGenerator() {
// eslint-disable-next-line
const self = this || state
onPending.apply(self, arguments)
generator.value = wrappedFn.apply(self, arguments)
yield generator.value
// @NOTE always async promise when wrapped with wrapper
generator.value.then(result => {
const [error, response] = result
if (error) onError.apply(self, [error].concat(arguments))
else onDone.apply(self, [response].concat(arguments))
})
return state
}
}
// @NOTE this is the "not-wrapped" generator
function toGeneratorUnsafe(fn) {
return function*() {
const generator = Object.create(GeneratorProtocolPrototype)
generator.state = 'pending'
try {
generator.value = fn.apply(this, arguments)
generator.state = 'called'
yield generator.value
generator.done = true
generator.state = 'done'
} catch (error) {
generator.error = generator.value = error
generator.state = 'error'
generator.done = true
}
return generator
}
}
export { toGenerator, toGeneratorUnsafe }
export default {
toGenerator,
toGeneratorUnsafe,
}
// const { protect, unprotect, getRoot } = require('mobx-state-tree')
// function asyncs(fn) {
// const runInUnprotect = (store, fn) => {
// unprotect(store)
// const retVal = fn()
// protect(store)
// return retVal
// }
// return function(...args) {
// const store = getRoot(this)
// const generator = runInUnprotect(store, () => fn.bind(this, ...args)())
// return new Promise((resolve, reject) => {
// const step = value => {
// const item = runInUnprotect(
// store,
// () => (typeof value === 'function' ? generator(value) : value)
// )
// return (item instanceof Promise ? item : Promise.resolve(item))
// .then(step)
// .catch(reject)
// }
// step()
// })
// }
// }
// if (isPromise(protocolState.value)) {
// protocolState.value.then(result => {
// const [error, response] = result
// if (error) onError.apply(self, [error].concat(arguments))
// else onDone.apply(self, [response].concat(arguments))
// })
// }
// toGenerator = curry2(toGenerator)
// import { protect, unprotect, getRoot, types } from 'xmobx/mobx-state-tree'
// function asyncWrap(fn) {
// const runInUnprotect = (store, fn) => {
// unprotect(store)
// const retVal = fn()
// protect(store)
// return retVal
// }
// return function(...args) {
// const store = getRoot(this)
// const generator = runInUnprotect(store, () => fn.bind(this, ...args)())
//
// return new Promise((resolve, reject) => {
// const step = value => {
// const item = runInUnprotect(store, () => generator.next(value))
//
// if (item.done) return resolve(item.value);
// (item.value instanceof Promise
// ? item.value
// : Promise.resolve(item.value))
// .then(step)
// .catch(reject)
// }
// step()
// })
// }
// }
//
// const fetchProjects = process(function* () {
// // <- note the star, this a generator function!
// self.state = 'pending'
// try {
// // ... yield can be used in async/await style
// self.githubProjects = yield mockResponse()
// self.state = 'done'
// } catch (e) {
// // ... including try/catch error handling
// console.error('Failed to fetch projects', error)
// self.state = 'error'
// }
// // The action will return a promise that resolves to the returned value
// // (or rejects with anything thrown from the action)
// return self.githubProjects.length
// })