diff --git a/CHANGELOG.md b/CHANGELOG.md index 7694d7340..2d57db7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,14 @@ This is the extensive list of all changes. The changes mentioned here are discussed in detail in the [release highlights](https://medium.com/p/c1fbc08008da/), or were simply updated in the docs. * MobX 4 introduces separation between the production and non production build. The production build strips most typechecks, resulting in a faster and smaller build. Make sure to substitute process.env.NODE_ENV = "production" in your build process! If you are using MobX in a react project, you most probably already have set this up. Otherwise, they idea is explained [here](https://reactjs.org/docs/add-react-to-an-existing-app.html). -* Observable maps are now backed by real ES6 Maps. This means that any value can be used as key now, not just strings and numbers. +* Introduced `flow` to create a chain of async actions. This is the same function as [`asyncActions`](https://github.com/mobxjs/mobx-utils#asyncaction) of the mobx-utils package. (Note, unlike `asyncAction` and unlike in the beta, it can _not_ be used as decorator) * Introduced `decorate(thing, decorators)` to decorate classes or object without needing decorator syntax. * Introduced `onBecomeObserved` and `onBecomeUnobserved`. These API's enable hooking into the observability system and get notified about when an observable starts / stops becoming used. This is great to automaticaly fetch data from external resources, or stop doing so. * `computed` / `@computed` now accepts a `requiresReaction` option. If it set, the computed value will throw an exception if it is being read while not being tracked by some reaction. * To make `requiresReaction` the default, use `mobx.configure({ computedRequiresReaction: true })` * Introduced `mobx.configure({ disableErrorBoundaries })`, for easier debugging of exceptoins. By [NaridaL](https://github.com/NaridaL) through [#1262](https://github.com/mobxjs/mobx/pull/1262) * `toJS` now accepts the options: `{ detectCycles?: boolean, exportMapsAsObjects?: boolean }`, both `true` by default -* Introduced `flow` to create a chain of async actions. This is the same function as [`asyncActions`](https://github.com/mobxjs/mobx-utils#asyncaction) of the mobx-utils package +* Observable maps are now backed by real ES6 Maps. This means that any value can be used as key now, not just strings and numbers. * The flow typings have been updated. Since this is a manual effort, there can be mistakes, so feel free to PR! * `computed(fn, options?)` / `@computed(options) get fn()` now accept the following options: diff --git a/src/api/flow.ts b/src/api/flow.ts index c1b07dd7f..b620f22fe 100644 --- a/src/api/flow.ts +++ b/src/api/flow.ts @@ -1,13 +1,6 @@ -import { BabelDescriptor } from "../utils/decorators2" -import { addHiddenFinalProp } from "../utils/utils" import { action } from "./action" -// method decorator: -export function flow( - target: Object, - propertyKey: string, - descriptor: PropertyDescriptor -): PropertyDescriptor +let generatorId = 0 // non-decorator forms export function flow(generator: () => IterableIterator): () => Promise @@ -43,132 +36,11 @@ export function flow( generator: (a1: A1, a2: A2) => IterableIterator ): (a1: A1, a2: A2) => Promise export function flow(generator: (a1: A1) => IterableIterator): (a1: A1) => Promise -// ... with name -export function flow(name: string, generator: () => IterableIterator): () => Promise -export function flow( - name: string, - generator: (a1: A1) => IterableIterator -): (a1: A1) => Promise // Ideally we want to have R instead of Any, but cannot specify R without specifying A1 etc... 'any' as result is better then not specifying request args -export function flow( - name: string, - generator: ( - a1: A1, - a2: A2, - a3: A3, - a4: A4, - a5: A5, - a6: A6, - a7: A7, - a8: A8 - ) => IterableIterator -): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8) => Promise -export function flow( - name: string, - generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => IterableIterator -): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => Promise -export function flow( - name: string, - generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => IterableIterator -): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => Promise -export function flow( - name: string, - generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => IterableIterator -): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => Promise -export function flow( - name: string, - generator: (a1: A1, a2: A2, a3: A3, a4: A4) => IterableIterator -): (a1: A1, a2: A2, a3: A3, a4: A4) => Promise -export function flow( - name: string, - generator: (a1: A1, a2: A2, a3: A3) => IterableIterator -): (a1: A1, a2: A2, a3: A3) => Promise -export function flow( - name: string, - generator: (a1: A1, a2: A2) => IterableIterator -): (a1: A1, a2: A2) => Promise -export function flow( - name: string, - generator: (a1: A1) => IterableIterator -): (a1: A1) => Promise - -/** - * `asyncAction` takes a generator function and automatically wraps all parts of the process in actions. See the examples below. - * `asyncAction` can be used both as decorator or to wrap functions. - * - * - It is important that `asyncAction should always be used with a generator function (recognizable as `function*` or `*name` syntax) - * - Each yield statement should return a Promise. The generator function will continue as soon as the promise settles, with the settled value - * - When the generator function finishes, you can return a normal value. The `asyncAction` wrapped function will always produce a promise delivering that value. - * - * When using the mobx devTools, an asyncAction will emit `action` events with names like: - * * `"fetchUsers - runid: 6 - init"` - * * `"fetchUsers - runid: 6 - yield 0"` - * * `"fetchUsers - runid: 6 - yield 1"` - * - * The `runId` represents the generator instance. In other words, if `fetchUsers` is invoked multiple times concurrently, the events with the same `runid` belong toghether. - * The `yield` number indicates the progress of the generator. `init` indicates spawning (it won't do anything, but you can find the original arguments of the `asyncAction` here). - * `yield 0` ... `yield n` indicates the code block that is now being executed. `yield 0` is before the first `yield`, `yield 1` after the first one etc. Note that yield numbers are not determined lexically but by the runtime flow. - * - * `asyncActions` requires `Promise` and `generators` to be available on the target environment. Polyfill `Promise` if needed. Both TypeScript and Babel can compile generator functions down to ES5. - * - * N.B. due to a [babel limitation](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy/issues/26), in Babel generatos cannot be combined with decorators. See also [#70](https://github.com/mobxjs/mobx-utils/issues/70) - * - * @example - * import {asyncAction} from "mobx-utils" - * - * let users = [] - * - * const fetchUsers = asyncAction("fetchUsers", function* (url) { - * const start = Date.now() - * const data = yield window.fetch(url) - * users = yield data.json() - * return start - Date.now() - * }) - * - * fetchUsers("http://users.com").then(time => { - * console.dir("Got users", users, "in ", time, "ms") - * }) - * - * @example - * import {asyncAction} from "mobx-utils" - * - * mobx.useStrict(true) // don't allow state modifications outside actions - * - * class Store { - * \@observable githubProjects = [] - * \@state = "pending" // "pending" / "done" / "error" - * - * \@asyncAction - * *fetchProjects() { // <- note the star, this a generator function! - * this.githubProjects = [] - * this.state = "pending" - * try { - * const projects = yield fetchGithubProjectsSomehow() // yield instead of await - * const filteredProjects = somePreprocessing(projects) - * // the asynchronous blocks will automatically be wrapped actions - * this.state = "done" - * this.githubProjects = filteredProjects - * } catch (error) { - * this.state = "error" - * } - * } - * } - * - * @export - * @returns {Promise} - */ -export function flow(arg1: any, arg2?: any): any { - // decorator - if (typeof arguments[1] === "string") return flowDecorator.apply(null, arguments) +export function flow(generator: Function) { + if (arguments.length !== 1) + fail(process.env.NODE_ENV && `Flow expects one 1 argument and cannot be used as decorator`) + const name = generator.name || "" - // direct invocation - const generator = typeof arg1 === "string" ? arg2 : arg1 - const name = typeof arg1 === "string" ? arg1 : generator.name || "" - return createFlowGenerator(name, generator) -} - -let generatorId = 0 - -export function createFlowGenerator(name: string, generator: Function) { // Implementation based on https://github.com/tj/co/blob/master/index.js return function() { const ctx = this @@ -216,24 +88,3 @@ export function createFlowGenerator(name: string, generator: Function) { }) } } - -function flowDecorator(target: any, propertyName: string, descriptor: BabelDescriptor) { - return { - configurable: true, - enumerable: false, - get() { - addHiddenFinalProp( - this, - propertyName, - flow( - propertyName, - descriptor.initializer ? descriptor.initializer.call(this) : descriptor.value - ) - ) - return this[propertyName] - }, - set(v: any) { - addHiddenFinalProp(this, propertyName, flow(propertyName, v)) - } - } -} diff --git a/test/base/babel-tests.js b/test/base/babel-tests.js index ff60b17f9..8e2995221 100644 --- a/test/base/babel-tests.js +++ b/test/base/babel-tests.js @@ -1073,7 +1073,7 @@ test("actions are not reassignable", () => { }).toThrow(/Cannot assign to read only property 'm4'/) }) -test("it should support asyncAction as decorator (babel)", async () => { +test("it should support asyncAction (babel)", async () => { const values = [] mobx.configure({ enforceActions: true }) @@ -1081,13 +1081,12 @@ test("it should support asyncAction as decorator (babel)", async () => { class X { @observable a = 1 - @mobx.flow - f = function*(initial) { + f = mobx.flow(function* f(initial) { this.a = initial // this runs in action this.a += yield Promise.resolve(5) this.a = this.a * 2 - return Promise.resolve(this.a) - } + return this.a + }) } const x = new X() diff --git a/test/base/flow.js b/test/base/flow.js index 82e3ed0cd..9db435d7b 100644 --- a/test/base/flow.js +++ b/test/base/flow.js @@ -89,15 +89,15 @@ test("it should support throw from yielded promise generator", done => { ) }) -test("it should support asyncAction as decorator", done => { +test("it should support asyncAction in classes", done => { const values = [] mobx.configure({ enforceActions: true }) class X { - a = 1; + a = 1 - *f(initial) { + f = mobx.flow(function*(initial) { this.a = initial // this runs in action try { this.a = yield delay(100, 5, true) // and this as well! @@ -107,11 +107,10 @@ test("it should support asyncAction as decorator", done => { this.a = e } return this.a - } + }) } mobx.decorate(X, { - a: mobx.observable, - f: mobx.flow + a: mobx.observable }) const x = new X() @@ -132,7 +131,7 @@ test("it should support logging", done => { const events = [] const x = mobx.observable({ a: 1 }) - const f = mobx.flow("myaction", function*(initial) { + const f = mobx.flow(function* myaction(initial) { x.a = initial x.a = yield delay(100, 5) x.a = 4 diff --git a/test/base/typescript-tests.ts b/test/base/typescript-tests.ts index 773de3579..b7a1b1ce0 100644 --- a/test/base/typescript-tests.ts +++ b/test/base/typescript-tests.ts @@ -1558,21 +1558,20 @@ test("promised when can be cancelled", async () => { } }) -test("it should support asyncAction as decorator (babel)", async () => { +test("it should support asyncAction as decorator (ts)", async () => { const values = [] mobx.configure({ enforceActions: true }) class X { - @observable a = 1; + @observable a = 1 - @mobx.flow - *f(initial: number): any { + f = mobx.flow(function* f(initial: number): any { this.a = initial // this runs in action this.a += yield Promise.resolve(5) this.a = this.a * 2 return this.a - } + }) } const x = new X()