Made mobx-utils compatible with MobX 6

This commit is contained in:
Michel Weststrate
2020-08-21 21:45:52 +01:00
parent 4c00265501
commit c2330f8a8d
26 changed files with 116 additions and 1945 deletions

View File

@@ -1,3 +1,11 @@
# 6.0.0
* [BREAKING] Dropped previously deprecated `asyncAction`. Use `mobx.flow` instead.
* [BREAKING] Dropped previously deprecated `actionAsync`. Use `mobx.flow` + `mobx.flowResult` instead.
* [BREAKING] Dropped previously deprecated `whenAsync`. Use `mobx.when` instead.
* [BREAKING] Dropped previously deprecated `whenWithTimeout`. Use `mobx.when` instead.
* [BREAKING] Added support for MobX 6.0.0. Minimim required MobX version is 6.0.0.
# 5.6.1
* [#256](https://github.com/mobxjs/mobx-utils/pull/256) Fix [#255](https://github.com/mobxjs/mobx-utils/issues/255)
@@ -6,7 +14,7 @@
* [#245](https://github.com/mobxjs/mobx-utils/pull/245) Add [ObservableGroupMap](https://github.com/mobxjs/mobx-utils#observablegroupmap).
* [#250](https://github.com/mobxjs/mobx-utils/pull/250) Fix [#249](https://github.com/mobxjs/mobx-utils/issues/249): lazyObservable: pending.set not wrapped in allowStateChanges.
* [#251](https://github.com/mobxjs/mobx-utils/pull/251) Fix fromStream initialValue not typed correctly.
* [#251](https://github.com/mobxjs/mobx-utils/pull/251) Fix fromStream initialValue not typed correctly.
# 5.5.7

233
README.md
View File

@@ -47,49 +47,37 @@ CDN: <https://unpkg.com/mobx-utils/mobx-utils.umd.js>
- [createViewModel](#createviewmodel)
- [Parameters](#parameters-6)
- [Examples](#examples-5)
- [whenWithTimeout](#whenwithtimeout)
- [keepAlive](#keepalive)
- [Parameters](#parameters-7)
- [Examples](#examples-6)
- [keepAlive](#keepalive)
- [keepAlive](#keepalive-1)
- [Parameters](#parameters-8)
- [Examples](#examples-7)
- [keepAlive](#keepalive-1)
- [queueProcessor](#queueprocessor)
- [Parameters](#parameters-9)
- [Examples](#examples-8)
- [queueProcessor](#queueprocessor)
- [chunkProcessor](#chunkprocessor)
- [Parameters](#parameters-10)
- [Examples](#examples-9)
- [chunkProcessor](#chunkprocessor)
- [now](#now)
- [Parameters](#parameters-11)
- [Examples](#examples-10)
- [now](#now)
- [expr](#expr)
- [Parameters](#parameters-12)
- [Examples](#examples-11)
- [asyncAction](#asyncaction)
- [deepObserve](#deepobserve)
- [Parameters](#parameters-13)
- [Examples](#examples-12)
- [whenAsync](#whenasync)
- [ObservableGroupMap](#observablegroupmap)
- [Parameters](#parameters-14)
- [Examples](#examples-13)
- [expr](#expr)
- [Parameters](#parameters-15)
- [Examples](#examples-14)
- [deepObserve](#deepobserve)
- [Parameters](#parameters-16)
- [Examples](#examples-15)
- [ObservableGroupMap](#observablegroupmap)
- [Parameters](#parameters-17)
- [Examples](#examples-16)
- [dispose](#dispose)
- [ObservableMap](#observablemap)
- [computedFn](#computedfn)
- [Parameters](#parameters-18)
- [Examples](#examples-17)
- [Parameters](#parameters-15)
- [Examples](#examples-14)
- [DeepMapEntry](#deepmapentry)
- [DeepMap](#deepmap)
- [actionAsync](#actionasync)
- [Parameters](#parameters-19)
- [Examples](#examples-18)
## fromPromise
@@ -408,40 +396,6 @@ viewModel.reset()
// prints "Get tea, Get tea", changes of the viewModel have been abandoned
```
## whenWithTimeout
Like normal `when`, except that this `when` will automatically dispose if the condition isn't met within a certain amount of time.
### Parameters
- `expr`
- `action`
- `timeout` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** maximum amount when spends waiting before giving up (optional, default `10000`)
- `onTimeout` **any** the ontimeout handler will be called if the condition wasn't met within the given time (optional, default `()=>{}`)
### Examples
```javascript
test("expect store to load", t => {
const store = {
items: [],
loaded: false
}
fetchDataForStore((data) => {
store.items = data;
store.loaded = true;
})
whenWithTimeout(
() => store.loaded
() => t.end()
2000,
() => t.fail("store didn't load with 2 secs")
)
})
```
Returns **IDisposer** disposer function that can be used to cancel the when prematurely. Neither action or onTimeout will be fired if disposed
## keepAlive
MobX normally suspends any computed value that is not in use by any reaction,
@@ -573,102 +527,6 @@ autorun(() => {
})
```
## asyncAction
_deprecated_ this functionality can now be found as `flow` in the mobx package. However, `flow` is not applicable as decorator, where `asyncAction` still is.
`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 together.
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)
### Parameters
- `arg1`
- `arg2`
### Examples
```javascript
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")
})
```
```javascript
import {asyncAction} from "mobx-utils"
mobx.configure({ enforceActions: "observed" }) // don't allow state modifications outside actions
class Store {
@observable githubProjects = []
@observable = "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"
}
}
}
```
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)**
## whenAsync
_deprecated_ whenAsync is deprecated, use mobx.when without effect instead.
Like normal `when`, except that this `when` will return a promise that resolves when the expression becomes truthy
### Parameters
- `fn`
- `timeout` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** maximum amount of time to wait, before the promise rejects
### Examples
```javascript
await whenAsync(() => !state.someBoolean)
```
Returns **any** Promise for when an observable eventually matches some condition. Rejects if timeout is provided and has expired
## expr
expr can be used to create temporarily views inside views.
@@ -807,74 +665,3 @@ console.log(store.m(3) * store.c)
## DeepMapEntry
## DeepMap
## actionAsync
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`.
When using the mobx devTools, an asyncAction will emit `action` events with names like:
- `"fetchUsers - runid 6 - step 0"`
- `"fetchUsers - runid 6 - step 1"`
- `"fetchUsers - runid 6 - step 2"`
The `runId` represents the action instance. In other words, if `fetchUsers` is invoked
multiple times concurrently, the events with the same `runid` belong together.
The `step` number indicates the code block that is now being executed.
### Parameters
- `arg1`
- `arg2`
- `arg3`
### Examples
```javascript
import {actionAsync, task} 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())
return start - Date.now()
})
const time = await fetchUsers("http://users.com")
console.log("Got users", users, "in ", time, "ms")
```
```javascript
import {actionAsync, task} from "mobx-utils"
mobx.configure({ enforceActions: "observed" }) // don't allow state modifications outside actions
class Store {
@observable githubProjects = []
@observable = "pending" // "pending" / "done" / "error"
@actionAsync
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
// note the use of task when awaiting!
const projects = await task(fetchGithubProjectsSomehow())
const filteredProjects = somePreprocessing(projects)
// the asynchronous blocks will automatically be wrapped actions
this.state = "done"
this.githubProjects = filteredProjects
} catch (error) {
this.state = "error"
}
}
}
```

View File

@@ -1,6 +1,6 @@
{
"name": "mobx-utils",
"version": "5.6.1",
"version": "6.0.0",
"description": "Utility functions and common patterns for MobX",
"main": "mobx-utils.umd.js",
"module": "mobx-utils.module.js",
@@ -42,7 +42,7 @@
"lodash.clonedeep": "*",
"lodash.clonedeepwith": "*",
"lodash.intersection": "*",
"mobx": "^5.15.4",
"mobx": "^6.0.0-rc.3",
"prettier": "^2.0.5",
"rollup": "^2.10.8",
"rxjs": "^6.5.5",
@@ -52,7 +52,7 @@
},
"dependencies": {},
"peerDependencies": {
"mobx": "^4.13.1 || ^5.13.1"
"mobx": "^6.0.0"
},
"keywords": [
"mobx",

View File

@@ -1,279 +0,0 @@
import { _startAction, _endAction, IActionRunInfo } from "mobx"
import { invariant } from "./utils"
import { decorateMethodOrField } from "./decorator-utils"
import { fail } from "./utils"
let runId = 0
const unfinishedIds = new Set<number>()
const currentlyActiveIds = new Set<number>()
interface IActionAsyncContext {
runId: number
step: number
actionRunInfo: IActionRunInfo
actionName: string
scope: any
args: IArguments
}
let inOrderExecution: () => Promise<void>
{
let taskOrderPromise: Promise<any> = Promise.resolve()
let queueMicrotaskPolyfill: typeof queueMicrotask
if (typeof queueMicrotask !== "undefined") {
// use real implementation if possible in modern browsers/node
queueMicrotaskPolyfill = queueMicrotask
} else if (typeof process !== "undefined" && process.nextTick) {
// fallback to node's process.nextTick in node <= 10
queueMicrotaskPolyfill = (cb: any) => {
process.nextTick(cb)
}
} else {
// use setTimeout for old browsers
queueMicrotaskPolyfill = (cb: any) => {
setTimeout(cb, 0)
}
}
const idle = () =>
new Promise((r) => {
queueMicrotaskPolyfill(r)
})
// we use this trick to force a proper order of execution
// even for immediately resolved promises
inOrderExecution = () => {
taskOrderPromise = taskOrderPromise.then(idle)
return taskOrderPromise
}
}
const actionAsyncContextStack: IActionAsyncContext[] = []
export async function task<R>(this: any, value: R | PromiseLike<R>): Promise<R> {
const ctx = actionAsyncContextStack[actionAsyncContextStack.length - 1]
if (!ctx) {
fail(
"'actionAsync' context not present when running 'task'. did you await inside an 'actionAsync' without using 'task(promise)'? did you forget to await the task?"
)
}
const { runId, actionName, args, scope, actionRunInfo, step } = ctx
const nextStep = step + 1
actionAsyncContextStack.pop()
_endAction(actionRunInfo)
currentlyActiveIds.delete(runId)
try {
const ret = await value
await inOrderExecution()
return ret
} catch (err) {
await inOrderExecution()
throw err
} finally {
// only restart if it not a dangling promise (the action is not yet finished)
if (unfinishedIds.has(runId)) {
const actionRunInfo = _startAction(
getActionAsyncName(actionName, runId, nextStep),
this,
args
)
actionAsyncContextStack.push({
runId,
step: nextStep,
actionRunInfo,
actionName,
args,
scope,
})
currentlyActiveIds.add(runId)
}
}
}
// method decorator
export function actionAsync(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor
// field decorator
export function actionAsync(target: object, propertyKey: string): void
// non-decorator forms
export function actionAsync<F extends (...args: any[]) => Promise<any>>(name: string, fn: F): F
export function actionAsync<F extends (...args: any[]) => Promise<any>>(fn: F): F
// base
/**
* 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`.
*
* When using the mobx devTools, an asyncAction will emit `action` events with names like:
* * `"fetchUsers - runid 6 - step 0"`
* * `"fetchUsers - runid 6 - step 1"`
* * `"fetchUsers - runid 6 - step 2"`
*
* The `runId` represents the action instance. In other words, if `fetchUsers` is invoked
* multiple times concurrently, the events with the same `runid` belong together.
* The `step` number indicates the code block that is now being executed.
*
* @example
* import {actionAsync, task} 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())
* 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"
*
* mobx.configure({ enforceActions: "observed" }) // don't allow state modifications outside actions
*
* class Store {
* \@observable githubProjects = []
* \@observable = "pending" // "pending" / "done" / "error"
*
* \@actionAsync
* async fetchProjects() {
* this.githubProjects = []
* this.state = "pending"
* try {
* // note the use of task when awaiting!
* const projects = await task(fetchGithubProjectsSomehow())
* const filteredProjects = somePreprocessing(projects)
* // the asynchronous blocks will automatically be wrapped actions
* this.state = "done"
* this.githubProjects = filteredProjects
* } catch (error) {
* this.state = "error"
* }
* }
* }
*/
export function actionAsync(arg1?: any, arg2?: any, arg3?: any): any {
// decorator
if (typeof arguments[1] === "string") {
return decorateMethodOrField(
"@actionAsync",
(prop, v) => {
return actionAsyncFn(prop, v)
},
arg1,
arg2,
arg3
)
}
// direct invocation
const actionName = typeof arg1 === "string" ? arg1 : arg1.name || "<unnamed action>"
const fn = typeof arg1 === "function" ? arg1 : arg2
return actionAsyncFn(actionName, fn)
}
function actionAsyncFn(actionName: string, fn: Function): Function {
if (!_startAction || !_endAction) {
fail("'actionAsync' requires mobx >=5.13.1 or >=4.13.1")
}
invariant(typeof fn === "function", "'asyncAction' expects a function")
if (typeof actionName !== "string" || !actionName)
fail(`actions should have valid names, got: '${actionName}'`)
return async function (this: any, ...args: any) {
const nextRunId = runId++
unfinishedIds.add(nextRunId)
const actionRunInfo = _startAction(getActionAsyncName(actionName, nextRunId, 0), this, args)
actionAsyncContextStack.push({
runId: nextRunId,
step: 0,
actionRunInfo,
actionName,
args,
scope: this,
})
currentlyActiveIds.add(nextRunId)
const finish = (err: any) => {
unfinishedIds.delete(nextRunId)
const ctx = actionAsyncContextStack.pop()
if (!ctx || ctx.runId !== nextRunId) {
// push it back if invalid
if (ctx) {
actionAsyncContextStack.push(ctx)
}
let msg = `invalid 'actionAsync' context when finishing action '${actionName}'.`
if (!ctx) {
msg += " no action context could be found instead."
} else {
msg += ` an action context for '${ctx.actionName}' was found instead.`
}
msg +=
" did you await inside an 'actionAsync' without using 'task(promise)'? did you forget to await the task?"
fail(msg)
}
ctx.actionRunInfo.error = err
_endAction(ctx.actionRunInfo)
currentlyActiveIds.delete(nextRunId)
if (err) {
throw err
}
}
let promise: any
try {
promise = fn.apply(this, args)
} catch (err) {
finish(err)
}
// are we done sync? (no task run)
if (currentlyActiveIds.has(nextRunId)) {
finish(undefined)
return promise
}
let ret: any
try {
ret = await promise
} catch (err) {
finish(err)
}
finish(undefined)
return ret
}
}
function getActionAsyncName(actionName: string, runId: number, step: number) {
return `${actionName} - runid ${runId} - step ${step}`
}

View File

@@ -20,7 +20,7 @@ export function moveItem<T>(target: IObservableArray<T>, fromIndex: number, toIn
if (fromIndex === toIndex) {
return
}
const oldItems = (target as any)[$mobx].values
const oldItems = target.slice()
let newItems: T[]
if (fromIndex < toIndex) {
newItems = [
@@ -53,7 +53,7 @@ function checkIndex(target: IObservableArray<any>, index: number) {
if (index < 0) {
throw new Error(`[mobx.array] Index out of bounds: ${index} is negative`)
}
const length = (target as any)[$mobx].values.length
const length = (target as any).length
if (index >= length) {
throw new Error(`[mobx.array] Index out of bounds: ${index} is not smaller than ${length}`)
}

View File

@@ -1,197 +0,0 @@
import { flow } from "mobx"
import { deprecated } from "./utils"
// method decorator:
export function asyncAction(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor
// non-decorator forms
export function asyncAction<R>(generator: () => IterableIterator<any>): () => Promise<R>
export function asyncAction<A1>(
generator: (a1: A1) => IterableIterator<any>
): (a1: A1) => Promise<any> // 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 asyncAction<A1, A2, A3, A4, A5, A6, A7, A8>(
generator: (
a1: A1,
a2: A2,
a3: A3,
a4: A4,
a5: A5,
a6: A6,
a7: A7,
a8: A8
) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8) => Promise<any>
export function asyncAction<A1, A2, A3, A4, A5, A6, A7>(
generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => Promise<any>
export function asyncAction<A1, A2, A3, A4, A5, A6>(
generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => Promise<any>
export function asyncAction<A1, A2, A3, A4, A5>(
generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => Promise<any>
export function asyncAction<A1, A2, A3, A4>(
generator: (a1: A1, a2: A2, a3: A3, a4: A4) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4) => Promise<any>
export function asyncAction<A1, A2, A3>(
generator: (a1: A1, a2: A2, a3: A3) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3) => Promise<any>
export function asyncAction<A1, A2>(
generator: (a1: A1, a2: A2) => IterableIterator<any>
): (a1: A1, a2: A2) => Promise<any>
export function asyncAction<A1>(
generator: (a1: A1) => IterableIterator<any>
): (a1: A1) => Promise<any>
// ... with name
export function asyncAction<R>(
name: string,
generator: () => IterableIterator<any>
): () => Promise<R>
export function asyncAction<A1>(
name: string,
generator: (a1: A1) => IterableIterator<any>
): (a1: A1) => Promise<any> // 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 asyncAction<A1, A2, A3, A4, A5, A6, A7, A8>(
name: string,
generator: (
a1: A1,
a2: A2,
a3: A3,
a4: A4,
a5: A5,
a6: A6,
a7: A7,
a8: A8
) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8) => Promise<any>
export function asyncAction<A1, A2, A3, A4, A5, A6, A7>(
name: string,
generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => Promise<any>
export function asyncAction<A1, A2, A3, A4, A5, A6>(
name: string,
generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => Promise<any>
export function asyncAction<A1, A2, A3, A4, A5>(
name: string,
generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => Promise<any>
export function asyncAction<A1, A2, A3, A4>(
name: string,
generator: (a1: A1, a2: A2, a3: A3, a4: A4) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3, a4: A4) => Promise<any>
export function asyncAction<A1, A2, A3>(
name: string,
generator: (a1: A1, a2: A2, a3: A3) => IterableIterator<any>
): (a1: A1, a2: A2, a3: A3) => Promise<any>
export function asyncAction<A1, A2>(
name: string,
generator: (a1: A1, a2: A2) => IterableIterator<any>
): (a1: A1, a2: A2) => Promise<any>
export function asyncAction<A1>(
name: string,
generator: (a1: A1) => IterableIterator<any>
): (a1: A1) => Promise<any>
/**
* _deprecated_ this functionality can now be found as `flow` in the mobx package. However, `flow` is not applicable as decorator, where `asyncAction` still is.
*
*
*
* `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 together.
* 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.configure({ enforceActions: "observed" }) // don't allow state modifications outside actions
*
* class Store {
* \@observable githubProjects = []
* \@observable = "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 asyncAction(arg1: any, arg2?: any): any {
// decorator
if (typeof arguments[1] === "string") {
const name = arguments[1]
const descriptor: PropertyDescriptor = arguments[2]
if (descriptor && descriptor.value) {
return Object.assign({}, descriptor, {
value: flow(descriptor.value),
})
} else {
return Object.assign({}, descriptor, {
set(v: any) {
Object.defineProperty(this, name, {
...descriptor,
value: flow(v),
})
},
})
}
}
// direct invocation
const generator = typeof arg1 === "string" ? arg2 : arg1
const name = typeof arg1 === "string" ? arg1 : generator.name || "<unnamed async action>"
deprecated("asyncAction is deprecated. use mobx.flow instead")
return flow(generator) // name get's dropped..
}

View File

@@ -12,6 +12,7 @@ import {
keys,
_getAdministration,
$mobx,
makeObservable,
} from "mobx"
import { invariant, getAllMethodsAndProperties } from "./utils"
@@ -38,10 +39,11 @@ export class ViewModel<T> implements IViewModel<T> {
@computed
get changedValues() {
return this.localValues.toJS()
return new Map(this.localValues)
}
constructor(public model: T) {
makeObservable(this)
invariant(isObservableObject(model), "createViewModel expects an observable object")
// use this helper as Object.getOwnPropertyNames doesn't return getters
@@ -54,8 +56,8 @@ export class ViewModel<T> implements IViewModel<T> {
`The propertyname ${key} is reserved and cannot be used with viewModels`
)
if (isComputedProp(model, key)) {
const derivation = _getAdministration(model, key).derivation // Fixme: there is no clear api to get the derivation
this.localComputedValues.set(key, computed(derivation.bind(this)))
const derivation: () => any = _getAdministration(model, key).derivation // Fixme: there is no clear api to get the derivation
this.localComputedValues.set(key, computed(derivation.bind(this)) as any)
}
const descriptor = Object.getOwnPropertyDescriptor(model, key)

View File

@@ -4,15 +4,14 @@ import {
isObservableObject,
isObservableArray,
IObjectDidChange,
IArrayChange,
IArraySplice,
IArrayDidChange,
IMapDidChange,
values,
entries,
} from "mobx"
import { IDisposer } from "./utils"
type IChange = IObjectDidChange | IArrayChange | IArraySplice | IMapDidChange
type IChange = IObjectDidChange | IArrayDidChange | IMapDidChange
type Entry = {
dispose: IDisposer

View File

@@ -1,44 +0,0 @@
import { when } from "mobx"
import { IDisposer, deprecated } from "./utils"
/**
* Like normal `when`, except that this `when` will automatically dispose if the condition isn't met within a certain amount of time.
*
* @example
* test("expect store to load", t => {
* const store = {
* items: [],
* loaded: false
* }
* fetchDataForStore((data) => {
* store.items = data;
* store.loaded = true;
* })
* whenWithTimeout(
* () => store.loaded
* () => t.end()
* 2000,
* () => t.fail("store didn't load with 2 secs")
* )
* })
*
*
* @export
* @param {() => boolean} expr see when, the expression to await
* @param {() => void} action see when, the action to execut when expr returns truthy
* @param {number} [timeout=10000] maximum amount when spends waiting before giving up
* @param {any} [onTimeout=() => {}] the ontimeout handler will be called if the condition wasn't met within the given time
* @returns {IDisposer} disposer function that can be used to cancel the when prematurely. Neither action or onTimeout will be fired if disposed
*/
export function whenWithTimeout(
expr: () => boolean,
action: () => void,
timeout: number = 10000,
onTimeout = () => {}
): IDisposer {
deprecated("whenWithTimeout is deprecated, use mobx.when with timeout option instead")
return when(expr, action, {
timeout,
onError: onTimeout,
})
}

View File

@@ -1,4 +1,4 @@
import { IComputedValue, getAtom } from "mobx"
import { IComputedValue, getAtom, observe } from "mobx"
import { IDisposer } from "./utils"
export function keepAlive(target: Object, property: string): IDisposer
@@ -37,5 +37,5 @@ export function keepAlive(_1: any, _2?: string) {
throw new Error(
"No computed provided, please provide an object created with `computed(() => expr)` or an object + property name"
)
return computed.observe(() => {})
return observe(computed, () => {})
}

View File

@@ -4,17 +4,13 @@ export * from "./lazy-observable"
export * from "./from-resource"
export * from "./observable-stream"
export * from "./create-view-model"
export * from "./guarded-when"
export * from "./keep-alive"
export * from "./queue-processor"
export * from "./chunk-processor"
export * from "./now"
export * from "./utils"
export * from "./async-action"
export * from "./when-async"
export * from "./expr"
export * from "./create-transformer"
export * from "./deepObserve"
export { ObservableGroupMap } from "./ObservableGroupMap"
export { computedFn } from "./computedFn"
export { actionAsync, task } from "./action-async"

View File

@@ -1,4 +1,4 @@
import { computed, observable, action, runInAction } from "mobx"
import { computed, observable, action, runInAction, observe, makeObservable } from "mobx"
declare var Symbol: any
@@ -54,16 +54,18 @@ export function toStream<T>(
subscribe(observer?: IStreamObserver<T> | ((value: T) => void) | null): ISubscription {
if ("function" === typeof observer) {
return {
unsubscribe: computedValue.observe(
({ newValue }: { newValue: T }) => observer(newValue),
unsubscribe: observe(
computedValue,
({ newValue }: { newValue: any }) => observer(newValue),
fireImmediately
),
}
}
if (observer && "object" === typeof observer && observer.next) {
return {
unsubscribe: computedValue.observe(
({ newValue }: { newValue: T }) => observer.next!(newValue),
unsubscribe: observe(
computedValue,
({ newValue }: { newValue: any }) => observer.next!(newValue),
fireImmediately
),
}
@@ -83,6 +85,7 @@ class StreamListener<T> implements IStreamObserver<T> {
subscription!: ISubscription
constructor(observable: IObservableStream<T>, initialValue: T) {
makeObservable(this)
runInAction(() => {
this.current = initialValue
this.subscription = observable.subscribe(this)

View File

@@ -12,13 +12,6 @@ export function invariant(cond: boolean, message = "Illegal state") {
if (!cond) fail(message)
}
const deprecatedMessages: string[] = []
export function deprecated(msg: string) {
if (deprecatedMessages.indexOf(msg) !== -1) return
deprecatedMessages.push(msg)
console.error("[mobx-utils] Deprecated: " + msg)
}
export function addHiddenProp(object: any, propName: string, value: any) {
Object.defineProperty(object, propName, {
enumerable: false,

View File

@@ -1,22 +0,0 @@
import { when } from "mobx"
import { deprecated } from "./utils"
/**
* _deprecated_ whenAsync is deprecated, use mobx.when without effect instead.
*
* Like normal `when`, except that this `when` will return a promise that resolves when the expression becomes truthy
*
* @example
* await whenAsync(() => !state.someBoolean)
*
* @export
* @param {() => boolean} fn see when, the expression to await
* @param {number} timeout maximum amount of time to wait, before the promise rejects
* @returns Promise for when an observable eventually matches some condition. Rejects if timeout is provided and has expired
*/
export function whenAsync(fn: () => boolean, timeout: number = 0): Promise<void> {
deprecated("whenAsync is deprecated, use mobx.when without effect instead")
return when(fn, {
timeout,
})
}

View File

@@ -4,7 +4,7 @@ import * as assert from "assert"
import { ObservableGroupMap } from "../src/mobx-utils"
const json = <G>(ogm: ObservableGroupMap<string, G>): { [k: string]: G } =>
Array.from(ogm.keys()).reduce((r, k) => ((r[k] = ogm.get(k)?.toJS()), r), {} as any)
Array.from(ogm.keys()).reduce((r, k) => ((r[k] = ogm.get(k)?.slice()), r), {} as any)
describe("ObservableGroupMap", () => {
type Slice = { day: string; hours: number }

View File

@@ -1,116 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`it should support logging 1`] = `
Array [
Object {
"arguments": Array [
1,
],
"name": "f - runid 7 - step 0",
"spyReportStart": true,
"type": "action",
},
Object {
"arguments": Array [
2,
],
"name": "innerF - runid 8 - step 0",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@37",
"newValue": 2,
"oldValue": 1,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
2,
],
"name": "innerF - runid 8 - step 1",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@37",
"newValue": 3,
"oldValue": 2,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"key": "a",
"name": "ObservableObject@37",
"newValue": 4,
"oldValue": 3,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
1,
],
"name": "f - runid 7 - step 1",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@37",
"newValue": 5,
"oldValue": 4,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
1,
],
"name": "f - runid 7 - step 2",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@37",
"newValue": 3,
"oldValue": 5,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
]
`;

View File

@@ -1,94 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`it should support logging 1`] = `
Array [
Object {
"arguments": Array [
2,
],
"name": "myaction - runid: 6 - init",
"spyReportStart": true,
"type": "action",
},
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
undefined,
],
"name": "myaction - runid: 6 - yield 0",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@10",
"newValue": 2,
"oldValue": 1,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
5,
],
"name": "myaction - runid: 6 - yield 1",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@10",
"newValue": 5,
"oldValue": 2,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"key": "a",
"name": "ObservableObject@10",
"newValue": 4,
"oldValue": 5,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
Object {
"arguments": Array [
3,
],
"name": "myaction - runid: 6 - yield 2",
"spyReportStart": true,
"type": "action",
},
Object {
"key": "a",
"name": "ObservableObject@10",
"newValue": 3,
"oldValue": 4,
"spyReportStart": true,
"type": "update",
},
Object {
"spyReportEnd": true,
},
Object {
"spyReportEnd": true,
},
]
`;

View File

@@ -4,7 +4,7 @@ exports[`make sure the fn is cached 1`] = `
Object {
"dependencies": Array [
Object {
"name": "ObservableObject@10.m",
"name": "ObservableObject@10.m?",
},
Object {
"dependencies": Array [

View File

@@ -5,9 +5,11 @@ Array [
Array [
"",
Object {
"debugObjectName": "ObservableObject@4",
"name": "a",
"newValue": 3,
"object": null,
"observableKind": "object",
"type": "add",
},
],
@@ -30,8 +32,10 @@ Array [
},
],
"addedCount": 2,
"debugObjectName": "ObservableArray@10",
"index": 1,
"object": null,
"observableKind": "array",
"removed": Array [
2,
],
@@ -42,9 +46,11 @@ Array [
Array [
"1",
Object {
"debugObjectName": "ObservableArray@10[..]",
"name": "x",
"newValue": "a",
"object": null,
"observableKind": "object",
"oldValue": 1,
"type": "update",
},
@@ -52,9 +58,11 @@ Array [
Array [
"2",
Object {
"debugObjectName": "ObservableArray@10[..]",
"name": "x",
"newValue": "b",
"object": null,
"observableKind": "object",
"oldValue": 2,
"type": "update",
},
@@ -62,9 +70,11 @@ Array [
Array [
"3",
Object {
"debugObjectName": "ObservableArray@10[..]",
"name": "x",
"newValue": "c",
"object": null,
"observableKind": "object",
"oldValue": 3,
"type": "update",
},
@@ -74,8 +84,10 @@ Array [
Object {
"added": Array [],
"addedCount": 0,
"debugObjectName": "ObservableArray@10",
"index": 0,
"object": null,
"observableKind": "array",
"removed": Array [
1,
Object {
@@ -101,8 +113,10 @@ Array [
},
],
"addedCount": 1,
"debugObjectName": "ObservableArray@10",
"index": 1,
"object": null,
"observableKind": "array",
"removed": Array [],
"removedCount": 0,
"type": "splice",
@@ -111,9 +125,11 @@ Array [
Array [
"0",
Object {
"debugObjectName": "ObservableArray@10[..]",
"name": "x",
"newValue": "A",
"object": null,
"observableKind": "object",
"oldValue": "c",
"type": "update",
},
@@ -126,6 +142,7 @@ Array [
Array [
"",
Object {
"debugObjectName": "ObservableObject@2",
"name": "a",
"newValue": 2,
"object": Object {
@@ -136,6 +153,7 @@ Array [
},
Symbol(mobx administration): null,
},
"observableKind": "object",
"oldValue": 1,
"type": "update",
},
@@ -143,12 +161,14 @@ Array [
Array [
"b",
Object {
"debugObjectName": "ObservableObject@2.b",
"name": "z",
"newValue": 4,
"object": Object {
"z": 4,
Symbol(mobx administration): null,
},
"observableKind": "object",
"oldValue": 3,
"type": "update",
},
@@ -161,12 +181,14 @@ Array [
Array [
"a",
Object {
"debugObjectName": "ObservableObject@6",
"name": "b",
"newValue": 2,
"object": Object {
"b": 2,
Symbol(mobx administration): null,
},
"observableKind": "object",
"oldValue": 1,
"type": "update",
},
@@ -174,10 +196,12 @@ Array [
Array [
"",
Object {
"debugObjectName": "ObservableObject@7",
"name": "a",
"object": Object {
Symbol(mobx administration): null,
},
"observableKind": "object",
"oldValue": Object {
"b": 2,
Symbol(mobx administration): null,
@@ -193,9 +217,11 @@ Array [
Array [
"a/b",
Object {
"debugObjectName": "ObservableObject@3.a.b",
"name": "c",
"newValue": 4,
"object": null,
"observableKind": "object",
"oldValue": 3,
"type": "update",
},
@@ -208,8 +234,10 @@ Array [
Array [
"",
Object {
"debugObjectName": "ObservableObject@5",
"name": "x",
"object": null,
"observableKind": "object",
"oldValue": 1,
"type": "remove",
},
@@ -222,30 +250,36 @@ Array [
Array [
"",
Object {
"debugObjectName": "ObservableObject@11",
"name": "x",
"newValue": Object {},
"newValue": Array [],
"object": null,
"observableKind": "object",
"type": "add",
},
],
Array [
"x",
Object {
"debugObjectName": "ObservableMap@12",
"name": "a",
"newValue": Object {
"a": 1,
Symbol(mobx administration): null,
},
"object": null,
"observableKind": "map",
"type": "add",
},
],
Array [
"x/a",
Object {
"debugObjectName": "ObservableMap@12.a",
"name": "a",
"newValue": 2,
"object": null,
"observableKind": "object",
"oldValue": 1,
"type": "update",
},
@@ -253,9 +287,11 @@ Array [
Array [
"x",
Object {
"debugObjectName": "ObservableMap@12",
"name": "a",
"newValue": 3,
"object": null,
"observableKind": "map",
"oldValue": Object {
"a": 2,
Symbol(mobx administration): null,
@@ -266,8 +302,10 @@ Array [
Array [
"x",
Object {
"debugObjectName": "ObservableMap@12",
"name": "a",
"object": null,
"observableKind": "map",
"oldValue": 3,
"type": "delete",
},

View File

@@ -1,685 +0,0 @@
import * as mobx from "mobx"
import { actionAsync, task } from "../src/mobx-utils"
function delay<T>(time: number, value: T) {
return new Promise<T>((resolve) => {
setTimeout(() => {
resolve(value)
}, time)
})
}
function delayThrow<T>(time: number, value: T) {
return new Promise<T>((_, reject) => {
setTimeout(() => {
reject(value)
}, time)
})
}
function delayFn(time: number, fn: () => void) {
return new Promise((resolve) => {
setTimeout(() => {
fn()
resolve()
}, time)
})
}
function expectNoActionsRunning() {
const obs = mobx.observable.box(1)
const d = mobx.reaction(
() => obs.get(),
() => {}
)
expect(() => obs.set(2)).toThrow(
"changing observed observable values outside actions is not allowed"
)
d()
}
test("it should support async actions", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f = actionAsync(async function (initial) {
x.a = initial // this runs in action
x.a = await task(delay(100, 3))
await task(delay(100, 0))
x.a = 4
x.a = await task(5)
expect(x.a).toBe(5)
return x.a
})
const v = await f(2)
expect(v).toBe(5)
expect(values).toEqual([1, 2, 3, 4, 5])
expectNoActionsRunning()
})
test("it should support try catch in async", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f = actionAsync(async function (initial) {
x.a = initial // this runs in action
try {
x.a = await task(delayThrow(100, 5))
await task(delay(100, 0))
x.a = 4
} catch (e) {
x.a = e
}
return x.a
})
const v = await f(2)
expect(v).toBe(5)
expect(values).toEqual([1, 2, 5])
expectNoActionsRunning()
})
test("it should support throw from async actions", async () => {
mobx.configure({ enforceActions: "observed" })
try {
await actionAsync(async () => {
await task(delay(10, 7))
throw 7
})()
fail("should fail")
} catch (e) {
expect(e).toBe(7)
}
expectNoActionsRunning()
})
test("it should support throw from awaited promise", async () => {
mobx.configure({ enforceActions: "observed" })
try {
await actionAsync(async () => {
return await task(delayThrow(10, 7))
})()
fail("should fail")
} catch (e) {
expect(e).toBe(7)
}
expectNoActionsRunning()
})
test("it should support async action in classes", async () => {
const values = []
mobx.configure({ enforceActions: "observed" })
class X {
a = 1
f = actionAsync(async function (initial) {
this.a = initial // this runs in action
try {
this.a = await task(delayThrow(100, 5))
await task(delay(100, 0))
this.a = 4
} catch (e) {
this.a = e
}
return this.a
})
}
mobx.decorate(X, {
a: mobx.observable,
})
const x = new X()
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const v = await x.f(2)
expect(v).toBe(5)
expect(values).toEqual([1, 2, 5])
expect(x.a).toBe(5)
expectNoActionsRunning()
})
test("it should support async action in classes with a method decorator", async () => {
const values = []
mobx.configure({ enforceActions: "observed" })
class X {
@mobx.observable a = 1
@actionAsync
async f(initial) {
this.a = initial // this runs in action
try {
this.a = await task(delayThrow(100, 5))
await task(delay(100, 0))
this.a = 4
} catch (e) {
this.a = e
}
return this.a
}
}
const x = new X()
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const v = await x.f(2)
expect(v).toBe(5)
expect(values).toEqual([1, 2, 5])
expect(x.a).toBe(5)
expectNoActionsRunning()
})
test("it should support async action in classes with a field decorator", async () => {
const values = []
mobx.configure({ enforceActions: "observed" })
class X {
@mobx.observable a = 1
@actionAsync
f = async (initial) => {
this.a = initial // this runs in action
try {
this.a = await task(delayThrow(100, 5))
await task(delay(100, 0))
this.a = 4
} catch (e) {
this.a = e
}
return this.a
}
}
const x = new X()
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const v = await x.f(2)
expect(v).toBe(5)
expect(values).toEqual([1, 2, 5])
expect(x.a).toBe(5)
expectNoActionsRunning()
})
test("it should support logging", async () => {
mobx.configure({ enforceActions: "observed" })
const events = []
const x = mobx.observable({ a: 1 })
const innerF = actionAsync("innerF", async (initial) => {
x.a = initial // this runs in action
x.a = await task(delay(100, 3))
x.a = 4
return x.a
})
const f = actionAsync("f", async (initial) => {
x.a = initial
x.a = await task(innerF(2))
x.a = 5
x.a = await task(delay(100, 3))
return x.a
})
const d = mobx.spy((ev) => events.push(ev))
await f(1)
expect(stripEvents(events)).toMatchSnapshot()
d()
expectNoActionsRunning()
})
function stripEvents(events) {
return events.map((e) => {
delete e.object
delete e.fn
delete e.time
return e
})
}
test("it should support async actions within async actions", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const innerF = actionAsync(async (initial) => {
x.a = initial // this runs in action
x.a = await task(delay(100, 3))
await task(delay(100, 0))
x.a = 4
return x.a
})
const f1 = actionAsync(async (initial) => {
x.a = await task(innerF(initial))
x.a = await task(delay(100, 5))
await task(delay(100, 0))
x.a = 6
return x.a
})
const v = await f1(2)
expect(v).toBe(6)
expect(values).toEqual([1, 2, 3, 4, 5, 6])
expectNoActionsRunning()
})
test("it should support async actions within async actions that are awaited later", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const innerF = actionAsync(async (initial) => {
x.a = initial // this runs in action
x.a = await task(delay(10, 3))
await task(delay(30, 0))
x.a = 6
return 7
})
const f1 = actionAsync(async (initial) => {
const futureInnerF = innerF(initial)
x.a = await task(delay(20, 4))
await task(delay(10, 0))
x.a = 5
x.a = await task(futureInnerF)
return x.a
})
const v = await f1(2)
expect(v).toBe(7)
expect(values).toEqual([1, 2, 3, 4, 5, 6, 7])
expectNoActionsRunning()
})
test("it should support async actions within async actions that throw", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const innerF = actionAsync(async function (initial) {
x.a = initial // this runs in action
x.a = await task(delay(100, 3))
await task(delay(100, 0))
x.a = 4
throw "err"
})
const f = actionAsync(async function (initial) {
x.a = await task(innerF(initial))
x.a = await task(delay(100, 5))
await task(delay(100, 0))
x.a = 6
return x.a
})
try {
await f(2)
fail("should fail")
} catch (e) {
expect(e).toBe("err")
}
expectNoActionsRunning()
})
test("typing", async () => {
const nothingAsync = async () => {
return [5]
}
const f = actionAsync(async (_initial: number) => {
const _n: number[] = await task(nothingAsync())
expect(_n).toEqual([5])
return "string"
})
const n: string = await f(5)
})
test("dangling promises created indirectly inside the action should be ok", async () => {
mobx.configure({ enforceActions: "observed" })
let danglingP
const f1 = actionAsync(async () => {
await task(
new Promise((resolve) => {
setTimeout(() => {
danglingP = delay(100, 1) // indirect dangling promise
resolve()
}, 100)
})
)
})
await f1()
expect(danglingP).toBeTruthy()
await danglingP
expectNoActionsRunning()
})
test("dangling promises created directly inside the action using task should NOT be ok", async () => {
mobx.configure({ enforceActions: "observed" })
let danglingP
const f1 = actionAsync("f1", async () => {
danglingP = task(delay(100, 1)) // dangling promise
})
try {
await f1()
fail("should fail")
} catch (err) {
expect(err.message).toBe(
"[mobx-utils] invalid 'actionAsync' context when finishing action 'f1'. no action context could be found instead. did you await inside an 'actionAsync' without using 'task(promise)'? did you forget to await the task?"
)
}
expectNoActionsRunning()
expect(danglingP).toBeTruthy()
await danglingP
expectNoActionsRunning()
})
test("dangling promises created directly inside the action without using task should be ok", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
let danglingP
const f1 = actionAsync(async () => {
danglingP = delay(100, 1) // dangling promise
x.a = 2
x.a = await task(delay(100, 3))
})
await f1()
expectNoActionsRunning()
expect(values).toEqual([1, 2, 3])
expect(danglingP).toBeTruthy()
await danglingP
expectNoActionsRunning()
})
test("it should support recursive async", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 10 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f1 = actionAsync(async () => {
if (x.a <= 0) return
x.a -= await task(delay(10, 1))
await task(f1())
})
await f1()
expect(values).toEqual([10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
expectNoActionsRunning()
})
test("it should support parallel async", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f1 = actionAsync(async () => {
x.a = 2
x.a = await task(delay(20, 6))
x.a = await task(delay(40, 9))
})
const f2 = actionAsync(async () => {
x.a = 3
x.a = await task(delay(10, 5))
x.a = await task(delay(30, 8))
})
const f3 = actionAsync(async () => {
x.a = 4 // 5
x.a = await task(delay(20, 7)) // 25
x.a = await task(delay(40, 10)) // 45
})
await Promise.all([
f1(),
f2(),
(async () => {
await delay(5, 0)
await f3()
})(),
(async () => {
expectNoActionsRunning()
})(),
delayFn(4, expectNoActionsRunning),
delayFn(6, expectNoActionsRunning),
delayFn(15, expectNoActionsRunning),
delayFn(24, expectNoActionsRunning),
delayFn(26, expectNoActionsRunning),
delayFn(35, expectNoActionsRunning),
delayFn(44, expectNoActionsRunning),
delayFn(46, expectNoActionsRunning),
])
expect(values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
expectNoActionsRunning()
})
test("calling async actions that do not await should be ok", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f1 = actionAsync("f1", async () => {
x.a++
})
const f2 = actionAsync("f2", async () => {
x.a++
})
await f1()
expectNoActionsRunning()
await f2()
expectNoActionsRunning()
await Promise.all([f1(), f2()])
expectNoActionsRunning()
expect(values).toEqual([1, 2, 3, 4, 5])
})
test("complex case", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f1 = actionAsync("f1", async (fn: any) => {
x.a++
await task(fn())
})
const f2 = async () => {
await f3()
}
const f3 = async () => {
await delay(10, 1)
await f4()
}
const f4 = async () => {
await f5()
}
const f5 = actionAsync("f5", async () => {
x.a += await task(delay(10, 1))
})
await f1(async () => {
await f2()
})
expectNoActionsRunning()
expect(values).toEqual([1, 2, 3])
})
test("immediately resolved promises", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f1 = actionAsync("f1", async () => {
await task(Promise.resolve(""))
x.a = await task(Promise.resolve(3))
})
const f2 = actionAsync("f2", async () => {
const f1Promise = f1()
x.a = 2
x.a = await task(Promise.resolve(3))
await task(f1Promise)
})
await f2()
expect(values).toEqual([1, 2, 3])
expectNoActionsRunning()
})
test("reusing promises", async () => {
mobx.configure({ enforceActions: "observed" })
const values = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const p = delay(10, 2)
const f1 = actionAsync("f1", async () => {
x.a = await task(p)
})
const f2 = actionAsync("f2", async () => {
x.a = (await task(p)) + 1
})
await Promise.all([f1(), f2()])
expect(values).toEqual([1, 2, 3])
expectNoActionsRunning()
})
test("actions that throw in parallel", async () => {
mobx.configure({ enforceActions: "observed" })
const r = (shouldThrow) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldThrow) {
reject("Error")
return
}
resolve(42)
}, 10)
})
const actionAsync1 = actionAsync("actionAsync1", async () => {
try {
return await task(r(true))
} catch (err) {
return "error"
}
})
const actionAsync2 = actionAsync("actionAsync2", async () => {
try {
return await task(r(false))
} catch (err) {
return "error"
}
})
const result = await Promise.all([actionAsync1(), actionAsync2(), actionAsync1()])
expectNoActionsRunning()
expect(result).toMatchInlineSnapshot(`
Array [
"error",
42,
"error",
]
`)
})

View File

@@ -1,178 +0,0 @@
import * as utils from "../src/mobx-utils"
import * as mobx from "mobx"
function delay<T>(time: number, value: T, shouldThrow = false): Promise<T> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldThrow) reject(value)
else resolve(value)
}, time)
})
}
test("it should support async generator actions", (done) => {
mobx.configure({ enforceActions: "observed" })
const values: any[] = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f = utils.asyncAction(function* (initial: number) {
x.a = initial // this runs in action
x.a = yield delay(100, 3) // and this as well!
yield delay(100, 0)
x.a = 4
return x.a
})
setTimeout(() => {
f(2).then((v: number) => {
// note: ideally, type of v should be inferred..
expect(v).toBe(4)
expect(values).toEqual([1, 2, 3, 4])
done()
})
}, 10)
})
test("it should support try catch in async generator", (done) => {
mobx.configure({ enforceActions: "observed" })
const values: any[] = []
const x = mobx.observable({ a: 1 })
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
const f = utils.asyncAction(function* (initial: number) {
x.a = initial // this runs in action
try {
x.a = yield delay(100, 5, true) // and this as well!
yield delay(100, 0)
x.a = 4
} catch (e) {
x.a = e
}
return x.a
})
setTimeout(() => {
f(2).then((v: number) => {
// note: ideally, type of v should be inferred..
expect(v).toBe(5)
expect(values).toEqual([1, 2, 5])
done()
})
}, 10)
})
test("it should support throw from async generator", (done) => {
utils
.asyncAction(function* () {
throw 7
})()
.then(
() => {
fail()
done()
},
(e) => {
expect(e).toBe(7)
done()
}
)
})
test("it should support throw from yielded promise generator", (done) => {
utils
.asyncAction(function* () {
return yield delay(10, 7, true)
})()
.then(
() => {
fail()
done()
},
(e) => {
expect(e).toBe(7)
done()
}
)
})
test("it should support asyncAction as decorator", (done) => {
const values: any[] = []
mobx.configure({ enforceActions: "observed" })
class X {
@mobx.observable a = 1;
@utils.asyncAction
*f(initial: number) {
this.a = initial // this runs in action
try {
this.a = yield delay(100, 5, true) // and this as well!
yield delay(100, 0)
this.a = 4
} catch (e) {
this.a = e
}
return this.a
}
}
const x = new X()
mobx.reaction(
() => x.a,
(v) => values.push(v),
{ fireImmediately: true }
)
setTimeout(() => {
// TODO: mweh on any cast...
;(x.f(/*test binding*/ 2) as any).then((v: number) => {
// note: ideally, type of v should be inferred..
expect(v).toBe(5)
expect(values).toEqual([1, 2, 5])
expect(x.a).toBe(5) // correct instance modified?
done()
})
}, 10)
})
test("it should support logging", (done) => {
mobx.configure({ enforceActions: "observed" })
const events: any[] = []
const x = mobx.observable({ a: 1 })
const f = utils.asyncAction(function* myaction(initial: number) {
x.a = initial
x.a = yield delay(100, 5)
x.a = 4
x.a = yield delay(100, 3)
return x.a
})
const d = mobx.spy((ev) => events.push(ev))
setTimeout(() => {
f(2).then(() => {
expect(stripEvents(events)).toMatchSnapshot()
d()
done()
})
}, 10)
})
function stripEvents(events) {
return events.map((e) => {
delete e.object
delete e.fn
delete e.time
return e
})
}

View File

@@ -62,8 +62,31 @@ test("transform1", () => {
expect(unloaded.length).toBe(1)
expect(unloaded[0][0]).toBe(tea)
expect(unloaded[0][1]).toBe("TEA")
expect((tea as any)[m.$mobx].values.get("title").observers.size).toBe(0)
expect((state.todos[0] as any)[m.$mobx].values.get("title").observers.size).toBe(1)
expect(m.getObserverTree(tea, "title")).toMatchInlineSnapshot(`
Object {
"name": "ObservableObject@1.todos[..].title",
}
`)
expect(m.getObserverTree(state.todos[0], "title")).toMatchInlineSnapshot(`
Object {
"name": "ObservableObject@1.todos[..].title",
"observers": Array [
Object {
"name": "Transformer--memoizationId:3",
"observers": Array [
Object {
"name": "Transformer--memoizationId:1",
"observers": Array [
Object {
"name": "Autorun@2",
},
],
},
],
},
],
}
`)
tea.title = "mint"
expect(mapped).toBe("johnBISCUIT")

View File

@@ -12,6 +12,9 @@ class TodoClass {
get usersCount(): number {
return this.usersInterested.length
}
constructor() {
mobx.makeObservable(this)
}
}
function Todo(title, done, usersInterested, unobservedProp) {

View File

@@ -1,67 +0,0 @@
"use strict"
const utils = require("../src/mobx-utils")
const mobx = require("mobx")
mobx.configure({ enforceActions: "observed" })
test("whenWithTimeout should operate normally", (done) => {
var a = mobx.observable.box(1)
utils.whenWithTimeout(
() => a.get() === 2,
() => done(),
500,
() => done.fail()
)
setTimeout(
mobx.action(() => a.set(2)),
200
)
})
test("whenWithTimeout should timeout", (done) => {
const a = mobx.observable.box(1)
utils.whenWithTimeout(
() => a.get() === 2,
() => done.fail("should have timed out"),
500,
() => done()
)
setTimeout(
mobx.action(() => a.set(2)),
1000
)
})
test("whenWithTimeout should dispose", (done) => {
const a = mobx.observable.box(1)
const d1 = utils.whenWithTimeout(
() => a.get() === 2,
() => done.fail("1 should not finsih"),
100,
() => done.fail("1 should not timeout")
)
const d2 = utils.whenWithTimeout(
() => a.get() === 2,
() => done.fail("2 should not finsih"),
200,
() => done.fail("2 should not timeout")
)
d1()
d2()
setTimeout(
mobx.action(() => {
a.set(2)
done()
}),
150
)
})

View File

@@ -8,7 +8,8 @@
"downlevelIteration": true,
"noEmit": true,
"rootDir": ".",
"lib": ["dom", "es2015", "scripthost"]
"lib": ["dom", "es2015", "scripthost"],
"useDefineForClassFields": true,
},
"include": ["**/*.ts"],
"exclude": ["/node_modules"]

View File

@@ -4607,10 +4607,10 @@ mkdirp@1.x:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mobx@^5.15.4:
version "5.15.4"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.4.tgz#9da1a84e97ba624622f4e55a0bf3300fb931c2ab"
integrity sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==
mobx@^6.0.0-rc.3:
version "6.0.0-rc.3"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.0.0-rc.3.tgz#ec1d0a820658932b93bdda4689969e70031000d2"
integrity sha512-56KAiSJJGCLTUJPz/M4SLISazAOS12NlxkYhO4qG2oWf3dwEDpwgTjT3kxt3Ac/YcFS+nWI9q/Y1wY7TM3uj+g==
module-deps-sortable@5.0.0:
version "5.0.0"