mirror of
https://github.com/zhigang1992/mobx-utils.git
synced 2026-04-29 04:35:03 +08:00
Made mobx-utils compatible with MobX 6
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -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
233
README.md
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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..
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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, () => {})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -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 [
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
`)
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -12,6 +12,9 @@ class TodoClass {
|
||||
get usersCount(): number {
|
||||
return this.usersInterested.length
|
||||
}
|
||||
constructor() {
|
||||
mobx.makeObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
function Todo(title, done, usersInterested, unobservedProp) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
@@ -8,7 +8,8 @@
|
||||
"downlevelIteration": true,
|
||||
"noEmit": true,
|
||||
"rootDir": ".",
|
||||
"lib": ["dom", "es2015", "scripthost"]
|
||||
"lib": ["dom", "es2015", "scripthost"],
|
||||
"useDefineForClassFields": true,
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["/node_modules"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user