Skip to content

Commit

Permalink
use a small promise polyfill so task is not needed
Browse files Browse the repository at this point in the history
  • Loading branch information
xaviergonz committed Oct 7, 2019
1 parent 63acece commit 38df465
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 60 deletions.
17 changes: 8 additions & 9 deletions README.md
Expand Up @@ -789,8 +789,9 @@ Alternative syntax for async actions, similar to `flow` but more compatible with
Typescript typings. Not to be confused with `asyncAction`, which is deprecated.

`actionAsync` can be used either as a decorator or as a function.
It takes an async function that internally must use `await task(promise)` rather than
the standard `await promise`.

It is _very_ important to await for any promises that are created _directly_ inside the async action,
or else an exception will be thrown.

When using the mobx devTools, an asyncAction will emit `action` events with names like:

Expand All @@ -811,15 +812,14 @@ The `step` number indicates the code block that is now being executed.
### Examples

```javascript
import {actionAsync, task} from "mobx-utils"
import {actionAsync} from "mobx-utils"

let users = []

const fetchUsers = actionAsync("fetchUsers", async (url) => {
const start = Date.now()
// note the use of task when awaiting!
const data = await task(window.fetch(url))
users = await task(data.json())
const data = await window.fetch(url)
users = await data.json()
return start - Date.now()
})

Expand All @@ -828,7 +828,7 @@ console.log("Got users", users, "in ", time, "ms")
```

```javascript
import {actionAsync, task} from "mobx-utils"
import {actionAsync} from "mobx-utils"

mobx.configure({ enforceActions: "observed" }) // don't allow state modifications outside actions

Expand All @@ -841,8 +841,7 @@ class Store {
this.githubProjects = []
this.state = "pending"
try {
// note the use of task when awaiting!
const projects = await task(fetchGithubProjectsSomehow())
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// the asynchronous blocks will automatically be wrapped actions
this.state = "done"
Expand Down
62 changes: 48 additions & 14 deletions src/action-async.ts
@@ -1,6 +1,7 @@
import { _startAction, _endAction, IActionRunInfo } from "mobx"
import { invariant } from "./utils"
import { decorateMethodOrField } from "./decorator-utils"
import { fail } from "./utils"

let runId = 0

Expand All @@ -17,14 +18,12 @@ const actionAsyncContextStack: IActionAsyncContext[] = []

function getCurrentActionAsyncContext() {
if (actionAsyncContextStack.length <= 0) {
fail(
"'actionAsync' context not present. did you await inside an 'actionAsync' without using 'task(promise)'?"
)
fail("'actionAsync' context not present")
}
return actionAsyncContextStack[actionAsyncContextStack.length - 1]!
}

export async function task<R>(promise: Promise<R>): Promise<R> {
async function task<R>(promise: Promise<R>): Promise<R> {
invariant(
typeof promise === "object" && typeof promise.then === "function",
"'task' expects a promise"
Expand Down Expand Up @@ -78,8 +77,9 @@ export function actionAsync<F extends (...args: any[]) => Promise<any>>(fn: F):
* Typescript typings. Not to be confused with `asyncAction`, which is deprecated.
*
* `actionAsync` can be used either as a decorator or as a function.
* It takes an async function that internally must use `await task(promise)` rather than
* the standard `await promise`.
*
* It is *very* important to await for any promises that are created *directly* inside the async action,
* or else an exception will be thrown.
*
* When using the mobx devTools, an asyncAction will emit `action` events with names like:
* * `"fetchUsers - runid 6 - step 0"`
Expand All @@ -91,23 +91,22 @@ export function actionAsync<F extends (...args: any[]) => Promise<any>>(fn: F):
* The `step` number indicates the code block that is now being executed.
*
* @example
* import {actionAsync, task} from "mobx-utils"
* import {actionAsync} from "mobx-utils"
*
* let users = []
*
* const fetchUsers = actionAsync("fetchUsers", async (url) => {
* const start = Date.now()
* // note the use of task when awaiting!
* const data = await task(window.fetch(url))
* users = await task(data.json())
* const data = await window.fetch(url)
* users = await data.json()
* return start - Date.now()
* })
*
* const time = await fetchUsers("http://users.com")
* console.log("Got users", users, "in ", time, "ms")
*
* @example
* import {actionAsync, task} from "mobx-utils"
* import {actionAsync} from "mobx-utils"
*
* mobx.configure({ enforceActions: "observed" }) // don't allow state modifications outside actions
*
Expand All @@ -120,8 +119,7 @@ export function actionAsync<F extends (...args: any[]) => Promise<any>>(fn: F):
* this.githubProjects = []
* this.state = "pending"
* try {
* // note the use of task when awaiting!
* const projects = await task(fetchGithubProjectsSomehow())
* const projects = await fetchGithubProjectsSomehow()
* const filteredProjects = somePreprocessing(projects)
* // the asynchronous blocks will automatically be wrapped actions
* this.state = "done"
Expand Down Expand Up @@ -187,7 +185,7 @@ function actionAsyncFn(actionName: string, fn: Function): Function {
const ctx = actionAsyncContextStack.pop()
if (!ctx || ctx.runId !== nextRunId) {
fail(
"'actionAsync' context not present or invalid. did you await inside an 'actionAsync' without using 'task(promise)'?"
"'actionAsync' context not present or invalid. did you forget to await a promise directly created inside the async action?"
)
}

Expand All @@ -200,3 +198,39 @@ function actionAsyncFn(actionName: string, fn: Function): Function {
function getActionAsyncName(actionName: string, runId: number, step: number) {
return `${actionName} - runid ${runId} - step ${step}`
}

let promisePolyfilled = false

function polyfillPromise() {
if (promisePolyfilled) {
return
}
promisePolyfilled = true

const OrigPromise: any = Promise

const MobxPromise = function Promise(this: any, ...args: any[]) {
const p = new OrigPromise(...args)

if (actionAsyncContextStack.length > 0) {
// inside an async action
return task(p)
} else {
return p
}
}

// hoist statics
for (const pname of Object.getOwnPropertyNames(OrigPromise)) {
const desc = Object.getOwnPropertyDescriptor(OrigPromise, pname)
Object.defineProperty(MobxPromise, pname, desc)
}
for (const pname of Object.getOwnPropertySymbols(OrigPromise)) {
const desc = Object.getOwnPropertyDescriptor(OrigPromise, pname)
Object.defineProperty(MobxPromise, pname, desc)
}

Promise = MobxPromise as any
}

polyfillPromise()
2 changes: 1 addition & 1 deletion src/mobx-utils.ts
Expand Up @@ -16,4 +16,4 @@ export * from "./expr"
export * from "./create-transformer"
export * from "./deepObserve"
export { computedFn } from "./computedFn"
export { actionAsync, task } from "./action-async"
export { actionAsync } from "./action-async"
6 changes: 5 additions & 1 deletion src/utils.ts
Expand Up @@ -4,8 +4,12 @@ export const NOOP = () => {}

export const IDENTITY = (_: any) => _

export function fail(message: string): never {
throw new Error("[mobx-utils] " + message)
}

export function invariant(cond: boolean, message = "Illegal state") {
if (!cond) throw new Error("[mobx-utils] " + message)
if (!cond) fail(message)
}

const deprecatedMessages: string[] = []
Expand Down
16 changes: 8 additions & 8 deletions test/__snapshots__/action-async.ts.snap
Expand Up @@ -35,6 +35,14 @@ Array [
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
1,
],
"name": "f - runid 7 - step 1",
"spyReportStart": true,
"type": "action",
},
Object {
"arguments": Array [
2,
Expand Down Expand Up @@ -68,14 +76,6 @@ Array [
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
1,
],
"name": "f - runid 7 - step 1",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@16",
Expand Down

0 comments on commit 38df465

Please sign in to comment.