? IWrappedComponent : never)
-export function inject(
+export function inject(
fn: IStoresToProps
): (target: T) => T & IWrappedComponent
diff --git a/packages/mobx-react/src/observer.tsx b/packages/mobx-react/src/observer.tsx
index 9f5c5eb7f7..a16bb13887 100644
--- a/packages/mobx-react/src/observer.tsx
+++ b/packages/mobx-react/src/observer.tsx
@@ -7,7 +7,12 @@ import { IReactComponent } from "./types/IReactComponent"
/**
* Observer function / decorator
*/
-export function observer(component: T): T {
+export function observer(component: T, context: ClassDecoratorContext): void
+export function observer(component: T): T
+export function observer(component: T, context?: ClassDecoratorContext): T {
+ if (context && context.kind !== "class") {
+ throw new Error("The @observer decorator can be used on classes only")
+ }
if (component["isMobxInjector"] === true) {
console.warn(
"Mobx observer: You are trying to use `observer` on a component that already has `inject`. Please apply `observer` before applying `inject`"
diff --git a/packages/mobx-react/src/observerClass.ts b/packages/mobx-react/src/observerClass.ts
index 716cb3371e..c428e8003f 100644
--- a/packages/mobx-react/src/observerClass.ts
+++ b/packages/mobx-react/src/observerClass.ts
@@ -1,159 +1,240 @@
-import { PureComponent, Component } from "react"
+import { PureComponent, Component, ComponentClass, ClassAttributes } from "react"
import {
- createAtom,
_allowStateChanges,
Reaction,
- $mobx,
_allowStateReadsStart,
- _allowStateReadsEnd
+ _allowStateReadsEnd,
+ _getGlobalState
} from "mobx"
-import { isUsingStaticRendering } from "mobx-react-lite"
+import {
+ isUsingStaticRendering,
+ _observerFinalizationRegistry as observerFinalizationRegistry
+} from "mobx-react-lite"
+import { shallowEqual, patch } from "./utils/utils"
-import { newSymbol, shallowEqual, setHiddenProp, patch } from "./utils/utils"
+const administrationSymbol = Symbol("ObserverAdministration")
+const isMobXReactObserverSymbol = Symbol("isMobXReactObserver")
-const mobxAdminProperty = $mobx || "$mobx"
-const mobxObserverProperty = newSymbol("isMobXReactObserver")
-const mobxIsUnmounted = newSymbol("isUnmounted")
-const skipRenderKey = newSymbol("skipRender")
-const isForcingUpdateKey = newSymbol("isForcingUpdate")
+let observablePropDescriptors: PropertyDescriptorMap
+if (__DEV__) {
+ observablePropDescriptors = {
+ props: createObservablePropDescriptor("props"),
+ state: createObservablePropDescriptor("state"),
+ context: createObservablePropDescriptor("context")
+ }
+}
+
+type ObserverAdministration = {
+ reaction: Reaction | null // also serves as disposed flag
+ forceUpdate: Function | null
+ mounted: boolean // we could use forceUpdate as mounted flag
+ reactionInvalidatedBeforeMount: boolean
+ name: string
+ // Used only on __DEV__
+ props: any
+ state: any
+ context: any
+}
+
+function getAdministration(component: Component): ObserverAdministration {
+ // We create administration lazily, because we can't patch constructor
+ // and the exact moment of initialization partially depends on React internals.
+ // At the time of writing this, the first thing invoked is one of the observable getter/setter (state/props/context).
+ return (component[administrationSymbol] ??= {
+ reaction: null,
+ mounted: false,
+ reactionInvalidatedBeforeMount: false,
+ forceUpdate: null,
+ name: getDisplayName(component.constructor as ComponentClass),
+ state: undefined,
+ props: undefined,
+ context: undefined
+ })
+}
export function makeClassComponentObserver(
- componentClass: React.ComponentClass
-): React.ComponentClass {
- const target = componentClass.prototype
+ componentClass: ComponentClass
+): ComponentClass {
+ const { prototype } = componentClass
- if (componentClass[mobxObserverProperty]) {
- const displayName = getDisplayName(target)
- console.warn(
- `The provided component class (${displayName})
- has already been declared as an observer component.`
+ if (componentClass[isMobXReactObserverSymbol]) {
+ const displayName = getDisplayName(componentClass)
+ throw new Error(
+ `The provided component class (${displayName}) has already been declared as an observer component.`
)
} else {
- componentClass[mobxObserverProperty] = true
+ componentClass[isMobXReactObserverSymbol] = true
}
- if (target.componentWillReact)
+ if (prototype.componentWillReact) {
throw new Error("The componentWillReact life-cycle event is no longer supported")
+ }
if (componentClass["__proto__"] !== PureComponent) {
- if (!target.shouldComponentUpdate) target.shouldComponentUpdate = observerSCU
- else if (target.shouldComponentUpdate !== observerSCU)
+ if (!prototype.shouldComponentUpdate) {
+ prototype.shouldComponentUpdate = observerSCU
+ } else if (prototype.shouldComponentUpdate !== observerSCU) {
// n.b. unequal check, instead of existence check, as @observer might be on superclass as well
throw new Error(
"It is not allowed to use shouldComponentUpdate in observer based components."
)
+ }
}
- // this.props and this.state are made observable, just to make sure @computed fields that
- // are defined inside the component, and which rely on state or props, re-compute if state or props change
- // (otherwise the computed wouldn't update and become stale on props change, since props are not observable)
- // However, this solution is not without it's own problems: https://github.com/mobxjs/mobx-react/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aobservable-props-or-not+
- makeObservableProp(target, "props")
- makeObservableProp(target, "state")
+ if (__DEV__) {
+ Object.defineProperties(prototype, observablePropDescriptors)
+ }
- const baseRender = target.render
- if (typeof baseRender !== "function") {
- const displayName = getDisplayName(target)
+ const originalRender = prototype.render
+ if (typeof originalRender !== "function") {
+ const displayName = getDisplayName(componentClass)
throw new Error(
`[mobx-react] class component (${displayName}) is missing \`render\` method.` +
`\n\`observer\` requires \`render\` being a function defined on prototype.` +
`\n\`render = () => {}\` or \`render = function() {}\` is not supported.`
)
}
- target.render = function () {
- return makeComponentReactive.call(this, baseRender)
+
+ prototype.render = function () {
+ Object.defineProperty(this, "render", {
+ // There is no safe way to replace render, therefore it's forbidden.
+ configurable: false,
+ writable: false,
+ value: isUsingStaticRendering()
+ ? originalRender
+ : createReactiveRender.call(this, originalRender)
+ })
+ return this.render()
}
- patch(target, "componentWillUnmount", function () {
- if (isUsingStaticRendering() === true) return
- this.render[mobxAdminProperty]?.dispose()
- this[mobxIsUnmounted] = true
-
- if (!this.render[mobxAdminProperty]) {
- // Render may have been hot-swapped and/or overriden by a subclass.
- const displayName = getDisplayName(this)
- console.warn(
- `The reactive render of an observer class component (${displayName})
- was overriden after MobX attached. This may result in a memory leak if the
- overriden reactive render was not properly disposed.`
+
+ const originalComponentDidMount = prototype.componentDidMount
+ prototype.componentDidMount = function () {
+ if (__DEV__ && this.componentDidMount !== Object.getPrototypeOf(this).componentDidMount) {
+ const displayName = getDisplayName(componentClass)
+ throw new Error(
+ `[mobx-react] \`observer(${displayName}).componentDidMount\` must be defined on prototype.` +
+ `\n\`componentDidMount = () => {}\` or \`componentDidMount = function() {}\` is not supported.`
)
}
+
+ // `componentDidMount` may not be called at all. React can abandon the instance after `render`.
+ // That's why we use finalization registry to dispose reaction created during render.
+ // Happens with `` see #3492
+ //
+ // `componentDidMount` can be called immediately after `componentWillUnmount` without calling `render` in between.
+ // Happens with ``see #3395.
+ //
+ // If `componentDidMount` is called, it's guaranteed to run synchronously with render (similary to `useLayoutEffect`).
+ // Therefore we don't have to worry about external (observable) state being updated before mount (no state version checking).
+ //
+ // Things may change: "In the future, React will provide a feature that lets components preserve state between unmounts"
+
+ const admin = getAdministration(this)
+
+ admin.mounted = true
+
+ // Component instance committed, prevent reaction disposal.
+ observerFinalizationRegistry.unregister(this)
+
+ // We don't set forceUpdate before mount because it requires a reference to `this`,
+ // therefore `this` could NOT be garbage collected before mount,
+ // preventing reaction disposal by FinalizationRegistry and leading to memory leak.
+ // As an alternative we could have `admin.instanceRef = new WeakRef(this)`, but lets avoid it if possible.
+ admin.forceUpdate = () => this.forceUpdate()
+
+ if (!admin.reaction || admin.reactionInvalidatedBeforeMount) {
+ // Missing reaction:
+ // 1. Instance was unmounted (reaction disposed) and immediately remounted without running render #3395.
+ // 2. Reaction was disposed by finalization registry before mount. Shouldn't ever happen for class components:
+ // `componentDidMount` runs synchronously after render, but our registry are deferred (can't run in between).
+ // In any case we lost subscriptions to observables, so we have to create new reaction and re-render to resubscribe.
+ // The reaction will be created lazily by following render.
+
+ // Reaction invalidated before mount:
+ // 1. A descendant's `componenDidMount` invalidated it's parent #3730
+
+ admin.forceUpdate()
+ }
+ return originalComponentDidMount?.apply(this, arguments)
+ }
+
+ // TODO@major Overly complicated "patch" is only needed to support the deprecated @disposeOnUnmount
+ patch(prototype, "componentWillUnmount", function () {
+ if (isUsingStaticRendering()) {
+ return
+ }
+ const admin = getAdministration(this)
+ admin.reaction?.dispose()
+ admin.reaction = null
+ admin.forceUpdate = null
+ admin.mounted = false
+ admin.reactionInvalidatedBeforeMount = false
})
+
return componentClass
}
// Generates a friendly name for debugging
-function getDisplayName(comp: any) {
- return (
- comp.displayName ||
- comp.name ||
- (comp.constructor && (comp.constructor.displayName || comp.constructor.name)) ||
- ""
- )
+function getDisplayName(componentClass: ComponentClass) {
+ return componentClass.displayName || componentClass.name || ""
}
-function makeComponentReactive(render: any) {
- if (isUsingStaticRendering() === true) return render.call(this)
-
- /**
- * If props are shallowly modified, react will render anyway,
- * so atom.reportChanged() should not result in yet another re-render
- */
- setHiddenProp(this, skipRenderKey, false)
- /**
- * forceUpdate will re-assign this.props. We don't want that to cause a loop,
- * so detect these changes
- */
- setHiddenProp(this, isForcingUpdateKey, false)
-
- const initialName = getDisplayName(this)
- const baseRender = render.bind(this)
-
- let isRenderingPending = false
-
- const reaction = new Reaction(`${initialName}.render()`, () => {
- if (!isRenderingPending) {
- // N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js)
- // This unidiomatic React usage but React will correctly warn about this so we continue as usual
- // See #85 / Pull #44
- isRenderingPending = true
- if (this[mobxIsUnmounted] !== true) {
- let hasError = true
- try {
- setHiddenProp(this, isForcingUpdateKey, true)
- if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this)
- hasError = false
- } finally {
- setHiddenProp(this, isForcingUpdateKey, false)
- if (hasError) reaction.dispose()
- }
- }
- }
- })
+function createReactiveRender(originalRender: any) {
+ const boundOriginalRender = originalRender.bind(this)
- reaction["reactComponent"] = this
- reactiveRender[mobxAdminProperty] = reaction
- this.render = reactiveRender
+ const admin = getAdministration(this)
function reactiveRender() {
- isRenderingPending = false
- let exception: unknown = undefined
- let rendering = undefined
- reaction.track(() => {
+ if (!admin.reaction) {
+ // Create reaction lazily to support re-mounting #3395
+ admin.reaction = createReaction(admin)
+ if (!admin.mounted) {
+ // React can abandon this instance and never call `componentDidMount`/`componentWillUnmount`,
+ // we have to make sure reaction will be disposed.
+ observerFinalizationRegistry.register(this, admin, this)
+ }
+ }
+
+ let error: unknown = undefined
+ let renderResult = undefined
+ admin.reaction.track(() => {
try {
- rendering = _allowStateChanges(false, baseRender)
+ // TODO@major
+ // Optimization: replace with _allowStateChangesStart/End (not available in mobx@6.0.0)
+ renderResult = _allowStateChanges(false, boundOriginalRender)
} catch (e) {
- exception = e
+ error = e
}
})
- if (exception) {
- throw exception
+ if (error) {
+ throw error
}
- return rendering
+ return renderResult
}
- return reactiveRender.call(this)
+ return reactiveRender
}
-function observerSCU(nextProps: React.Props, nextState: any): boolean {
+function createReaction(admin: ObserverAdministration) {
+ return new Reaction(`${admin.name}.render()`, () => {
+ if (!admin.mounted) {
+ // This is neccessary to avoid react warning about calling forceUpdate on component that isn't mounted yet.
+ // This happens when component is abandoned after render - our reaction is already created and reacts to changes.
+ // `componenDidMount` runs synchronously after `render`, so unlike functional component, there is no delay during which the reaction could be invalidated.
+ // However `componentDidMount` runs AFTER it's descendants' `componentDidMount`, which CAN invalidate the reaction, see #3730. Therefore remember and forceUpdate on mount.
+ admin.reactionInvalidatedBeforeMount = true
+ return
+ }
+
+ try {
+ admin.forceUpdate?.()
+ } catch (error) {
+ admin.reaction?.dispose()
+ admin.reaction = null
+ }
+ })
+}
+
+function observerSCU(nextProps: ClassAttributes, nextState: any): boolean {
if (isUsingStaticRendering()) {
console.warn(
"[mobx-react] It seems that a re-rendering of a React component is triggered while in static (server-side) mode. Please make sure components are rendered only once server-side."
@@ -170,45 +251,24 @@ function observerSCU(nextProps: React.Props, nextState: any): boolean {
return !shallowEqual(this.props, nextProps)
}
-function makeObservableProp(target: any, propName: string): void {
- const valueHolderKey = newSymbol(`reactProp_${propName}_valueHolder`)
- const atomHolderKey = newSymbol(`reactProp_${propName}_atomHolder`)
- function getAtom() {
- if (!this[atomHolderKey]) {
- setHiddenProp(this, atomHolderKey, createAtom("reactive " + propName))
- }
- return this[atomHolderKey]
- }
- Object.defineProperty(target, propName, {
+function createObservablePropDescriptor(key: "props" | "state" | "context") {
+ return {
configurable: true,
enumerable: true,
- get: function () {
- let prevReadState = false
-
- // Why this check? BC?
- // @ts-expect-error
- if (_allowStateReadsStart && _allowStateReadsEnd) {
- prevReadState = _allowStateReadsStart(true)
+ get() {
+ const admin = getAdministration(this)
+ const derivation = _getGlobalState().trackingDerivation
+ if (derivation && derivation !== admin.reaction) {
+ throw new Error(
+ `[mobx-react] Cannot read "${admin.name}.${key}" in a reactive context, as it isn't observable.
+ Please use component lifecycle method to copy the value into a local observable first.
+ See https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/README.md#note-on-using-props-and-state-in-derivations`
+ )
}
- getAtom.call(this).reportObserved()
-
- // Why this check? BC?
- // @ts-expect-error
- if (_allowStateReadsStart && _allowStateReadsEnd) {
- _allowStateReadsEnd(prevReadState)
- }
-
- return this[valueHolderKey]
+ return admin[key]
},
- set: function set(v) {
- if (!this[isForcingUpdateKey] && !shallowEqual(this[valueHolderKey], v)) {
- setHiddenProp(this, valueHolderKey, v)
- setHiddenProp(this, skipRenderKey, true)
- getAtom.call(this).reportChanged()
- setHiddenProp(this, skipRenderKey, false)
- } else {
- setHiddenProp(this, valueHolderKey, v)
- }
+ set(value) {
+ getAdministration(this)[key] = value
}
- })
+ }
}
diff --git a/packages/mobx-react/src/utils/utils.ts b/packages/mobx-react/src/utils/utils.ts
index c79efa4564..9571d3b6f0 100644
--- a/packages/mobx-react/src/utils/utils.ts
+++ b/packages/mobx-react/src/utils/utils.ts
@@ -1,21 +1,3 @@
-let symbolId = 0
-function createSymbol(name: string): symbol | string {
- if (typeof Symbol === "function") {
- return Symbol(name)
- }
- const symbol = `__$mobx-react ${name} (${symbolId})`
- symbolId++
- return symbol
-}
-
-const createdSymbols = {}
-export function newSymbol(name: string): symbol | string {
- if (!createdSymbols[name]) {
- createdSymbols[name] = createSymbol(name)
- }
- return createdSymbols[name]
-}
-
export function shallowEqual(objA: any, objB: any): boolean {
//From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js
if (is(objA, objB)) {
@@ -96,8 +78,8 @@ export function setHiddenProp(target: object, prop: any, value: any): void {
* Utilities for patching componentWillUnmount, to make sure @disposeOnUnmount works correctly icm with user defined hooks
* and the handler provided by mobx-react
*/
-const mobxMixins = newSymbol("patchMixins")
-const mobxPatchedDefinition = newSymbol("patchedDefinition")
+const mobxMixins = Symbol("patchMixins")
+const mobxPatchedDefinition = Symbol("patchedDefinition")
export interface Mixins extends Record {
locks: number
@@ -175,6 +157,7 @@ function createDefinition(
let wrappedFunc = wrapFunction(originalMethod, mixins)
return {
+ // @ts-ignore
[mobxPatchedDefinition]: true,
get: function () {
return wrappedFunc
diff --git a/packages/mobx-undecorate/CHANGELOG.md b/packages/mobx-undecorate/CHANGELOG.md
index a49600aa7b..f8c0d7d62c 100644
--- a/packages/mobx-undecorate/CHANGELOG.md
+++ b/packages/mobx-undecorate/CHANGELOG.md
@@ -1,5 +1,15 @@
# mobx-undecorate
+## 1.3.0
+
+### Minor Changes
+
+- [`c8f3b081`](https://github.com/mobxjs/mobx/commit/c8f3b0817fd74644e285909e2a40cea45a5cc013) [#3478](https://github.com/mobxjs/mobx/pull/3478) Thanks [@urugator](https://github.com/urugator)! - partial fix #3460: replace `action` with `override` if field uses override keyword
+
+### Patch Changes
+
+- [`988aa3a1`](https://github.com/mobxjs/mobx/commit/988aa3a198f0e0fd33623cb21b33d75db6b2f70a) [#3617](https://github.com/mobxjs/mobx/pull/3617) Thanks [@urugator](https://github.com/urugator)! - fix: preserve non-null assertion operator on undecorated props
+
## 1.2.0
### Minor Changes
diff --git a/packages/mobx-undecorate/__tests__/cli.spec.tsx b/packages/mobx-undecorate/__tests__/cli.spec.tsx
index 826bc737e4..3ceede7a39 100644
--- a/packages/mobx-undecorate/__tests__/cli.spec.tsx
+++ b/packages/mobx-undecorate/__tests__/cli.spec.tsx
@@ -5,18 +5,18 @@ const dedent = require("dedent-js")
test("run cli #2506 #3142", () => {
// #3142 - the white space must be in cwd
- const cwd = join(__dirname, "fixtures", "some path");
+ const cwd = join(__dirname, "fixtures", "some path")
const testFile = join(cwd, "some file.tsx")
const baseContent = dedent(`import { observable } from "mobx";
class Test {
@observable x = 1;
}
- `)
+ `)
mkdirSync(dirname(testFile), { recursive: true })
writeFileSync(testFile, baseContent)
execSync("node ../../../cli.js", { cwd })
expect(readFileSync(testFile, "utf8")).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
+ "import { observable, makeObservable } from "mobx";
class Test {
x = 1;
@@ -64,8 +64,8 @@ test("run cli with --parseTsAsNonJsx #2754", () => {
cwd: join(__dirname, "fixtures")
})
expect(readFileSync(testNonJsxFile, "utf8")).toMatchInlineSnapshot(`
- "import { useSearch } from \\"./useSearch\\"
- import { observable, makeObservable } from \\"mobx\\";
+ "import { useSearch } from "./useSearch"
+ import { observable, makeObservable } from "mobx";
class Test {
x = 1;
@@ -79,7 +79,7 @@ test("run cli with --parseTsAsNonJsx #2754", () => {
export function useAlias(): string {
const parsedSearch = useSearch()
- return (parsedSearch.alias || \\"\\").toUpperCase()
+ return (parsedSearch.alias || "").toUpperCase()
}"
`)
expect(readFileSync(testJsxFile, "utf8")).toMatchInlineSnapshot(`
diff --git a/packages/mobx-undecorate/__tests__/tsconfig.json b/packages/mobx-undecorate/__tests__/tsconfig.json
new file mode 100644
index 0000000000..ecae17017d
--- /dev/null
+++ b/packages/mobx-undecorate/__tests__/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../../tsconfig.test.json"
+}
diff --git a/packages/mobx-undecorate/__tests__/undecorate.spec.ts b/packages/mobx-undecorate/__tests__/undecorate.spec.ts
index 76c4be7177..ca15d6952c 100644
--- a/packages/mobx-undecorate/__tests__/undecorate.spec.ts
+++ b/packages/mobx-undecorate/__tests__/undecorate.spec.ts
@@ -25,20 +25,20 @@ describe("general", () => {
field /*2 */ = /*3*/ 1 /*4*/
}`)
).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
-
- class Box {
- /*0*/
- /*1*/
- field /*2 */ = /*3*/ 1; /*4*/
-
- constructor() {
- makeObservable(this, {
- field: observable
- });
- }
- }"
- `)
+ "import { observable, makeObservable } from "mobx";
+
+ class Box {
+ /*0*/
+ /*1*/
+ field /*2 */ = /*3*/ 1; /*4*/
+
+ constructor() {
+ makeObservable(this, {
+ field: observable
+ });
+ }
+ }"
+ `)
})
test("basic observable - skip imports", () => {
@@ -109,7 +109,7 @@ describe("general", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
+ "import { observable, makeObservable } from "mobx";
class ExtendsHasMethod extends Box {
x = 1;
@@ -125,7 +125,7 @@ describe("general", () => {
// test
method() {
- console.log(\\"hi\\")
+ console.log("hi")
}
}"
`)
@@ -144,20 +144,20 @@ describe("general", () => {
}
}`)
).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
+ "import { observable, makeObservable } from "mobx";
- class ExtendsHasConstructor {
- x = 1;
+ class ExtendsHasConstructor {
+ x = 1;
- constructor() {
- makeObservable(this, {
- x: observable
- });
+ constructor() {
+ makeObservable(this, {
+ x: observable
+ });
- console.log(\\"hi\\")
- }
- }"
- `)
+ console.log("hi")
+ }
+ }"
+ `)
})
test("extended class with constructor", () => {
@@ -175,22 +175,22 @@ describe("general", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
+ "import { observable, makeObservable } from "mobx";
- class ExtendsHasConstructorSuper extends Box {
- x = 1;
+ class ExtendsHasConstructorSuper extends Box {
+ x = 1;
- constructor() {
- super()
+ constructor() {
+ super()
- makeObservable(this, {
- x: observable
- });
+ makeObservable(this, {
+ x: observable
+ });
- console.log(\\"hi\\")
- }
- }"
- `)
+ console.log("hi")
+ }
+ }"
+ `)
})
})
@@ -208,20 +208,20 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- x = (arg: number) => {
- console.log('hi')
- };
-
- constructor() {
- makeObservable(this, {
- x: action.bound(\\"test\\")
- });
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ x = (arg: number) => {
+ console.log('hi')
+ };
+
+ constructor() {
+ makeObservable(this, {
+ x: action.bound("test")
+ });
+ }
+ }"
+ `)
})
test("method - bound - named", () => {
@@ -238,21 +238,21 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: action.bound(\\"test\\")
- });
- }
-
- async x(arg: number): boolean {
- console.log('hi')
- return true
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: action.bound("test")
+ });
+ }
+
+ async x(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
})
test("method - bound - named - generator", () => {
@@ -269,21 +269,21 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: action.bound(\\"test\\")
- });
- }
-
- *x(arg: number): boolean {
- console.log('hi')
- return true
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: action.bound("test")
+ });
+ }
+
+ *x(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
})
test("field - named", () => {
@@ -300,21 +300,21 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- x = async (arg: number): boolean => {
- console.log('hi')
- return true
- };
-
- constructor() {
- makeObservable(this, {
- x: action(\\"test\\")
- });
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ x = async (arg: number): boolean => {
+ console.log('hi')
+ return true
+ };
+
+ constructor() {
+ makeObservable(this, {
+ x: action("test")
+ });
+ }
+ }"
+ `)
})
test("field - unnamed", () => {
@@ -331,21 +331,21 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- x = (arg: number): boolean => {
- console.log('hi')
- return true
- };
-
- constructor() {
- makeObservable(this, {
- x: action
- });
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ x = (arg: number): boolean => {
+ console.log('hi')
+ return true
+ };
+
+ constructor() {
+ makeObservable(this, {
+ x: action
+ });
+ }
+ }"
+ `)
})
test("method - bound - unnamed", () => {
@@ -362,21 +362,21 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: action.bound
- });
- }
-
- x(arg: number): boolean {
- console.log('hi')
- return true
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: action.bound
+ });
+ }
+
+ x(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
})
test("method - unbound - named", () => {
@@ -393,21 +393,21 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: action(\\"test\\")
- });
- }
-
- x(arg: number): boolean {
- console.log('hi')
- return true
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: action("test")
+ });
+ }
+
+ x(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
})
test("method - unbound - unnamed", () => {
@@ -424,21 +424,21 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: action
- });
- }
-
- x(arg: number): boolean {
- console.log('hi')
- return true
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: action
+ });
+ }
+
+ x(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
})
test("method - unbound - computed name", () => {
@@ -455,21 +455,96 @@ describe("action", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { action, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- ['x' + 'y']: action
- });
- }
-
- ['x' + 'y'](arg: number): boolean {
- console.log('hi')
- return true
- }
- }"
- `)
+ "import { action, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ ['x' + 'y']: action
+ });
+ }
+
+ ['x' + 'y'](arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
+ })
+
+ test("method - override", () => {
+ expect(
+ convert(`
+ import { action } from "mobx"
+
+ class Box extends Shape {
+ constructor(arg) {
+ super(arg)
+ }
+
+ @action
+ override method(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }
+ `)
+ ).toMatchInlineSnapshot(`
+ "import { action, override, makeObservable } from "mobx";
+
+ class Box extends Shape {
+ constructor(arg) {
+ super(arg)
+
+ makeObservable(this, {
+ method: override
+ });
+ }
+
+ override method(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
+ })
+
+ test("method - override - keepDecorators", () => {
+ expect(
+ convert(
+ `
+ import { action } from "mobx"
+
+ class Box extends Shape {
+ constructor(arg) {
+ super(arg)
+ }
+
+ @action
+ override method(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }
+ `,
+ { keepDecorators: true }
+ )
+ ).toMatchInlineSnapshot(`
+ "import { action, override, makeObservable } from "mobx";
+
+ class Box extends Shape {
+ constructor(arg) {
+ super(arg)
+ makeObservable(this);
+ }
+
+ @override
+ override method(arg: number): boolean {
+ console.log('hi')
+ return true
+ }
+ }"
+ `)
})
})
@@ -484,18 +559,18 @@ describe("observable", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
-
- class Box {
- x = 1;
-
- constructor() {
- makeObservable(this, {
- x: observable
- });
- }
- }"
- `)
+ "import { observable, makeObservable } from "mobx";
+
+ class Box {
+ x = 1;
+
+ constructor() {
+ makeObservable(this, {
+ x: observable
+ });
+ }
+ }"
+ `)
})
test("observable - shallow ", () => {
@@ -508,7 +583,7 @@ describe("observable", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
+ "import { observable, makeObservable } from "mobx";
class Box {
x = 1;
@@ -532,14 +607,14 @@ describe("observable", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { observable, makeObservable } from \\"mobx\\";
+ "import { observable, makeObservable } from "mobx";
class Box {
['x'] = 1;
constructor() {
makeObservable(this, {
- [\\"x\\"]: observable.shallow
+ ["x"]: observable.shallow
});
}
}"
@@ -560,20 +635,20 @@ describe("computed", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { computed, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: computed
- });
- }
-
- get x() {
- return 1;
- }
- }"
- `)
+ "import { computed, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: computed
+ });
+ }
+
+ get x() {
+ return 1;
+ }
+ }"
+ `)
})
test("computed - setter", () => {
@@ -591,23 +666,23 @@ describe("computed", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { computed, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: computed
- });
- }
-
- get x() {
- return 1;
- }
- set x(v) {
- console.log(v)
- }
- }"
- `)
+ "import { computed, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: computed
+ });
+ }
+
+ get x() {
+ return 1;
+ }
+ set x(v) {
+ console.log(v)
+ }
+ }"
+ `)
})
test("computed - setter - options", () => {
@@ -628,26 +703,26 @@ describe("computed", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { computed, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: computed({ name: \\"test\\" })
- });
- }
-
- get x() {
- return 1;
- }
- set y(z) {
- console.log(\\"wrong\\");
- }
- set x(v) {
- console.log(v)
- }
- }"
- `)
+ "import { computed, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: computed({ name: "test" })
+ });
+ }
+
+ get x() {
+ return 1;
+ }
+ set y(z) {
+ console.log("wrong");
+ }
+ set x(v) {
+ console.log(v)
+ }
+ }"
+ `)
})
test("computed - setter - struct", () => {
@@ -665,23 +740,23 @@ describe("computed", () => {
}
`)
).toMatchInlineSnapshot(`
- "import { computed, makeObservable } from \\"mobx\\";
-
- class Box {
- constructor() {
- makeObservable(this, {
- x: computed.struct
- });
- }
-
- get x() {
- return 1;
- }
- set x(v) {
- console.log(v)
- }
- }"
- `)
+ "import { computed, makeObservable } from "mobx";
+
+ class Box {
+ constructor() {
+ makeObservable(this, {
+ x: computed.struct
+ });
+ }
+
+ get x() {
+ return 1;
+ }
+ set x(v) {
+ console.log(v)
+ }
+ }"
+ `)
})
})
@@ -714,7 +789,7 @@ describe("decorate", () => {
})
`)
).toMatchInlineSnapshot(`
- "import { observable, computed, action, makeObservable } from \\"mobx\\"
+ "import { observable, computed, action, makeObservable } from "mobx"
class Box {
width = 3
@@ -768,9 +843,9 @@ describe("decorate", () => {
})
`)
).toMatchInlineSnapshot(`
- "import { observable, computed, action, makeObservable } from \\"mobx\\"
+ "import { observable, computed, action, makeObservable } from "mobx"
- test(\\"a\\", () => {
+ test("a", () => {
class Box {
width = 3
@@ -782,7 +857,7 @@ describe("decorate", () => {
}
})
- test(\\"b\\", () => {
+ test("b", () => {
class Box {
constructor() {
makeObservable(this, {
@@ -812,7 +887,7 @@ describe("decorate", () => {
})
`)
).toMatchInlineSnapshot(`
- "import { observable, computed, action, makeObservable } from \\"mobx\\"
+ "import { observable, computed, action, makeObservable } from "mobx"
class Box {
constructor() {
@@ -841,7 +916,7 @@ describe("decorate", () => {
})
`)
).toMatchInlineSnapshot(`
- "import { observable, computed, action, makeObservable } from \\"mobx\\"
+ "import { observable, computed, action, makeObservable } from "mobx"
const box = {
@@ -865,13 +940,13 @@ describe("decorate", () => {
})
`)
).toMatchInlineSnapshot(`
- "import { observable, computed, action, makeObservable } from \\"mobx\\"
+ "import { observable, computed, action, makeObservable } from "mobx"
- makeObservable({}, {
- width: observable,
- height: observable.shallow,
- })"
- `)
+ makeObservable({}, {
+ width: observable,
+ height: observable.shallow,
+ })"
+ `)
})
})
@@ -893,15 +968,15 @@ describe("privates", () => {
`
)
).toMatchInlineSnapshot(`
- "import { observable, computed, action, makeObservable } from \\"mobx\\"
+ "import { observable, computed, action, makeObservable } from "mobx"
class TryToGetThis {
private privateField1: number = 1;
protected privateField2 = 1;
- public publicField: string = \\"test\\";
+ public publicField: string = "test";
constructor() {
- makeObservable(this, {
+ makeObservable(this, {
privateField1: observable,
privateField2: observable,
publicField: observable
@@ -931,7 +1006,7 @@ describe("privates", () => {
}
)
).toMatchInlineSnapshot(`
- "import { observable, computed, action, makeObservable } from \\"mobx\\"
+ "import { observable, computed, action, makeObservable } from "mobx"
class TryToGetThis {
@observable
@@ -939,7 +1014,7 @@ describe("privates", () => {
@observable
protected privateField2 = 1
@observable
- public publicField: string = \\"test\\"
+ public publicField: string = "test"
constructor() {
makeObservable(this);
@@ -1041,7 +1116,7 @@ describe("@observer", () => {
/* 1 */
- export const X = inject(\\"test\\")(class /* 2 */ /* 3 */ X extends React.Component {
+ export const X = inject("test")(class /* 2 */ /* 3 */ X extends React.Component {
});"
`)
@@ -1064,7 +1139,7 @@ describe("@observer", () => {
/* 1 */
- export const X = inject(\\"test\\")(observer(class /* 2 */ /* 3 */ X extends React.Component {
+ export const X = inject("test")(observer(class /* 2 */ /* 3 */ X extends React.Component {
}));"
`)
@@ -1087,8 +1162,8 @@ describe("@observer", () => {
)
).toMatchInlineSnapshot(`
"import {observer, inject} from 'mobx-react'
- import { observable, makeObservable } from \\"mobx\\";
- import {Component} from \\"react\\"
+ import { observable, makeObservable } from "mobx";
+ import {Component} from "react"
const X = observer(class X extends React.Component {
field = 1;
@@ -1121,8 +1196,8 @@ describe("@observer", () => {
)
).toMatchInlineSnapshot(`
"import {observer, inject} from 'mobx-react'
- import { observable, makeObservable } from \\"mobx\\";
- import {PureComponent} from \\"react\\"
+ import { observable, makeObservable } from "mobx";
+ import {PureComponent} from "react"
const X = observer(class X extends PureComponent<{x: boolean}> {
field = 1;
@@ -1155,8 +1230,8 @@ describe("@observer", () => {
)
).toMatchInlineSnapshot(`
"import {observer, inject} from 'mobx-react'
- import { observable, makeObservable } from \\"mobx\\";
- import {PureComponent} from \\"react\\"
+ import { observable, makeObservable } from "mobx";
+ import {PureComponent} from "react"
@observer class X extends PureComponent<{x: boolean}> {
@observable field = 1
@@ -1301,7 +1376,7 @@ test("class comp with observer, inject and decorator from another package", () =
import {withRouter} from 'react-router-dom'
/* 1 */
export const X = withRouter(
- inject(\\"test\\")(observer(class /* 2 */ /* 3 */ X extends React.Component {}))
+ inject("test")(observer(class /* 2 */ /* 3 */ X extends React.Component {}))
);"
`)
})
@@ -1352,9 +1427,9 @@ test("class default export comp with observer and inject", () => {
`)
).toMatchInlineSnapshot(`
"import {observer, inject} from 'mobx-react'
-
+
class X extends React.Component {}
- export default inject(\\"test\\")(observer(X));"
+ export default inject("test")(observer(X));"
`)
})
@@ -1374,7 +1449,29 @@ test("class default export comp with observer and inject", () => {
import {withRouter} from 'react-router-dom'
class X extends React.Component {}
-
- export default withRouter(inject(\\"test\\")(observer(X)))"
+
+ export default withRouter(inject("test")(observer(X)))"
+ `)
+})
+
+test("non-null assertion operator is preserved", () => {
+ expect(
+ convert(`
+ import { observable } from 'mobx';
+ class X {
+ @observable todos!: ObservableMap
+ }
+ `)
+ ).toMatchInlineSnapshot(`
+ "import { observable, makeObservable } from 'mobx';
+ class X {
+ todos!: ObservableMap;
+
+ constructor() {
+ makeObservable(this, {
+ todos: observable
+ });
+ }
+ }"
`)
})
diff --git a/packages/mobx-undecorate/package.json b/packages/mobx-undecorate/package.json
index 18318f241e..4a546b14b3 100644
--- a/packages/mobx-undecorate/package.json
+++ b/packages/mobx-undecorate/package.json
@@ -1,6 +1,6 @@
{
"name": "mobx-undecorate",
- "version": "1.2.0",
+ "version": "1.3.0",
"description": "Migrate MobX 4/5 to MobX 6",
"bin": "cli.js",
"repository": {
@@ -25,6 +25,7 @@
"homepage": "https://mobx.js.org/",
"dependencies": {
"@babel/core": "^7.9.0",
+ "@babel/parser": "^7.18.9",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0",
@@ -35,7 +36,7 @@
"jscodeshift": "^0.11.0"
},
"devDependencies": {
- "@types/jscodeshift": "^0.7.0"
+ "@types/jscodeshift": "^0.11.6"
},
"keywords": [
"mobx"
diff --git a/packages/mobx-undecorate/src/undecorate.ts b/packages/mobx-undecorate/src/undecorate.ts
index 793427e327..4d6e090f8a 100644
--- a/packages/mobx-undecorate/src/undecorate.ts
+++ b/packages/mobx-undecorate/src/undecorate.ts
@@ -116,18 +116,24 @@ export default function transform(
const lines = fileInfo.source.split("\n")
let changed = false
let needsInitializeImport = false
- const decoratorsUsed = new Set(options?.ignoreImports ? validDecorators : [])
+ let importOverride = false
+ const decoratorsUsed = new Set(options?.ignoreImports ? validDecorators : [])
let usesDecorate = options?.ignoreImports ? true : false
let hasReact = options?.ignoreImports ? true : false
+ // error TS2321: Excessive stack depth comparing types 'ArrayType' and 'ArrayType'
+ // @ts-ignore
source.find(j.ImportDeclaration).forEach(im => {
- if (im.value.source.value === "react") hasReact = true
+ if (im.value.source.value === "react") {
+ hasReact = true
+ }
if (validPackages.includes(im.value.source.value as string)) {
let decorateIndex = -1
- im.value.specifiers.forEach((specifier, idx) => {
+ im.value.specifiers?.forEach((specifier, idx) => {
// imported decorator
if (
j.ImportSpecifier.check(specifier) &&
+ typeof specifier.imported.name === "string" && // dunno what IdentifierKind is
validDecorators.includes(specifier.imported.name)
) {
decoratorsUsed.add(specifier.imported.name)
@@ -139,7 +145,7 @@ export default function transform(
}
})
if (decorateIndex !== -1) {
- im.value.specifiers.splice(decorateIndex, 1)
+ im.value.specifiers?.splice(decorateIndex, 1)
}
}
})
@@ -163,7 +169,7 @@ export default function transform(
const decorators = callPath.value.arguments[1]
if (!j.Identifier.check(target)) {
- // not targetting a class, just swap it with makeObservable
+ // not targeting a class, just swap it with makeObservable
changed = true
// @ts-ignore // TODO: or "observable" ?
callPath.value.callee.name = "makeObservable"
@@ -180,7 +186,7 @@ export default function transform(
}
const targetDeclaration = declarations[0].parentPath.value
if (!j.ClassDeclaration.check(targetDeclaration)) {
- // not targetting a class, just swap it with makeObservable
+ // not targeting a class, just swap it with makeObservable
changed = true
// @ts-ignore // TODO: or "observable" ?
callPath.value.callee.name = "makeObservable"
@@ -223,8 +229,13 @@ export default function transform(
const { comments, ...k } = key
const { comments: comments2, ...v } = value
const prop = j.objectProperty(k, v)
+ if (v.name === "override") {
+ importOverride = true
+ }
prop.computed = !!computed
- if (isPrivate) privates.push(k.name)
+ if (isPrivate) {
+ privates.push(k.name)
+ }
return prop
})
)
@@ -254,6 +265,9 @@ export default function transform(
if (!mobxImport.specifiers) {
mobxImport.specifiers = []
}
+ if (importOverride) {
+ mobxImport.specifiers.push(j.importSpecifier(j.identifier("override")))
+ }
mobxImport.specifiers.push(j.importSpecifier(j.identifier("makeObservable")))
}
}
@@ -281,7 +295,9 @@ export default function transform(
dec.expression.callee.name === "inject"
const hasObserverOrInject = decorators.some(dec => isObserver(dec) || isInject(dec))
- if (!hasObserverOrInject) return
+ if (!hasObserverOrInject) {
+ return
+ }
// If module uses default export
if (defaultExportPath && clazz.id) {
@@ -299,13 +315,13 @@ export default function transform(
const exportDecl = j.exportDefaultDeclaration(newDefaultExportDefExpr.exported)
// re-create the class
- let newClassDefExpr = j.classExpression(clazz.id, clazz.body, clazz.superClass)
+ const newClassDefExpr = j.classExpression(clazz.id, clazz.body, clazz.superClass)
newClassDefExpr.superTypeParameters = clazz.superTypeParameters
newClassDefExpr.typeParameters = clazz.typeParameters
newClassDefExpr.implements = clazz.implements
const newClassDefDecl = j.classDeclaration(
- newClassDefExpr.id,
+ newClassDefExpr.id ?? null,
newClassDefExpr.body,
newClassDefExpr.superClass
)
@@ -346,7 +362,7 @@ export default function transform(
}, newClassDefExpr)
const decl = j.variableDeclaration("const", [
- j.variableDeclarator(j.identifier(clazz.id!.name), newClassDefExpr)
+ j.variableDeclarator(j.identifier(clazz.id!.name.toString()), newClassDefExpr)
])
decl.comments = clazz.comments
clazzPath.replace(decl)
@@ -377,7 +393,7 @@ export default function transform(
if (!j.Decorator.check(decorator)) {
return property
}
- const expr = decorator.expression
+ let expr = decorator.expression
if (j.Identifier.check(expr) && !decoratorsUsed.has(expr.name)) {
warn(`Found non-mobx decorator @${expr.name}`, decorator)
return property
@@ -387,12 +403,23 @@ export default function transform(
return property
}
- if (options?.keepDecorators !== true) property.decorators.splice(0)
+ if (options?.keepDecorators !== true) {
+ property.decorators.splice(0)
+ }
+
+ // Replace decorator with @override
+ if ((property as any).override) {
+ const overrideDecorator = j.decorator(j.identifier("override"))
+ if (options?.keepDecorators) {
+ property.decorators[0] = overrideDecorator
+ }
+ expr = overrideDecorator.expression
+ }
effects.membersMap.push([
property.key,
expr,
- property.computed,
+ property.computed ?? false,
property.accessibility === "private" || property.accessibility === "protected"
])
return property
@@ -411,6 +438,9 @@ export default function transform(
)
)
if (privates.length && !options?.keepDecorators) {
+ if (typeof clazz.id!.name !== "string") {
+ throw new Error("Unexpected type")
+ }
// @ts-ignore
initializeObservablesCall.expression.typeArguments = j.tsTypeParameterInstantiation([
j.tsTypeReference(j.identifier(clazz.id!.name)),
@@ -449,7 +479,9 @@ export default function transform(
let propsType = isReactComponent && clazz.superTypeParameters?.params[0]
const propsParam = j.identifier("props")
// reuse the generic if we found it
- if (propsType) propsParam.typeAnnotation = j.tsTypeAnnotation(propsType as any)
+ if (propsType) {
+ propsParam.typeAnnotation = j.tsTypeAnnotation(propsType as any)
+ }
// create the constructor
const constructorDecl = j.methodDefinition(
"constructor",
diff --git a/packages/mobx/CHANGELOG.md b/packages/mobx/CHANGELOG.md
index 304857116f..1604eae330 100644
--- a/packages/mobx/CHANGELOG.md
+++ b/packages/mobx/CHANGELOG.md
@@ -1,5 +1,208 @@
# mobx
+## 6.15.1
+
+### Patch Changes
+
+- [`df81c144fb148b64140d761aa61f032a7f429e12`](https://github.com/mobxjs/mobx/commit/df81c144fb148b64140d761aa61f032a7f429e12) [#4523](https://github.com/mobxjs/mobx/pull/4523) Thanks [@exzos28](https://github.com/exzos28)! - Make `FlowCancellationError` a proper `Error` instance while preserving its previous string representation.
+
+- [`21fc4de6c09a77caf115aedd2fe6df972637412b`](https://github.com/mobxjs/mobx/commit/21fc4de6c09a77caf115aedd2fe6df972637412b) [#4626](https://github.com/mobxjs/mobx/pull/4626) Thanks [@kubk](https://github.com/kubk)! - Export `CancellablePromise` from the public `mobx` entrypoint.
+
+## 6.15.0
+
+### Minor Changes
+
+- [`2e703388eda4ba3eefed2bf1f5ca3958980978c3`](https://github.com/mobxjs/mobx/commit/2e703388eda4ba3eefed2bf1f5ca3958980978c3) [#4584](https://github.com/mobxjs/mobx/pull/4584) Thanks [@mweststrate](https://github.com/mweststrate)! - Fix #4753: Wrong inferred type of observable.map in TS 5.9+, by @mbest in #578
+
+### Patch Changes
+
+- [`61d6cf39764f28c6c2e0d2b3912364889739c619`](https://github.com/mobxjs/mobx/commit/61d6cf39764f28c6c2e0d2b3912364889739c619) [#4563](https://github.com/mobxjs/mobx/pull/4563) Thanks [@mweststrate](https://github.com/mweststrate)! - Fixed memory leak where makeAutoObservable would keep wrapping setters defined on the prototype. Fixes #4553
+
+## 6.14.0
+
+### Minor Changes
+
+- [`fe1d3f45cfd0d21189e963579e5c9d764329fea9`](https://github.com/mobxjs/mobx/commit/fe1d3f45cfd0d21189e963579e5c9d764329fea9) [#4558](https://github.com/mobxjs/mobx/pull/4558) Thanks [@vkrol](https://github.com/vkrol)! - Add Explicit Resource Management support in reactions
+
+## 6.13.7
+
+### Patch Changes
+
+- [`54e3f71ca02f09b3107290f18d8484b70a6e2f0b`](https://github.com/mobxjs/mobx/commit/54e3f71ca02f09b3107290f18d8484b70a6e2f0b) [#4528](https://github.com/mobxjs/mobx/pull/4528) Thanks [@k-g-a](https://github.com/k-g-a)! - Fix observable.set not respecting the new value from interceptors
+
+## 6.13.6
+
+### Patch Changes
+
+- [`bca3841347f4fba50ad910e1c4176c56ba0173d1`](https://github.com/mobxjs/mobx/commit/bca3841347f4fba50ad910e1c4176c56ba0173d1) [#3993](https://github.com/mobxjs/mobx/pull/3993) Thanks [@tonyraoul](https://github.com/tonyraoul)! - Improve observableset memory footprint and performance
+
+## 6.13.5
+
+### Patch Changes
+
+- [`4c077738776d5fc7ba0f108805a9ec816c2709b9`](https://github.com/mobxjs/mobx/commit/4c077738776d5fc7ba0f108805a9ec816c2709b9) [#3943](https://github.com/mobxjs/mobx/pull/3943) Thanks [@tonyraoul](https://github.com/tonyraoul)! - Fix browser compatability issue introduced in 6.13.4 release
+
+## 6.13.4
+
+### Patch Changes
+
+- [`f91d2e1dc85a53b729c03bd28ab39ffcf0838403`](https://github.com/mobxjs/mobx/commit/f91d2e1dc85a53b729c03bd28ab39ffcf0838403) [#3935](https://github.com/mobxjs/mobx/pull/3935) Thanks [@tonyraoul](https://github.com/tonyraoul)! - Update typescript version to 5.6.2 and added support for esnext iterator helpers
+
+## 6.13.3
+
+### Patch Changes
+
+- [`a1cf2c63ef92d3d42a5b42a23ff6c7a745664cfd`](https://github.com/mobxjs/mobx/commit/a1cf2c63ef92d3d42a5b42a23ff6c7a745664cfd) [#3902](https://github.com/mobxjs/mobx/pull/3902) Thanks [@jzhan-canva](https://github.com/jzhan-canva)! - Fix 2022.3 @action decorators on fields no longer require makeObservable
+
+## 6.13.2
+
+### Patch Changes
+
+- [`f1f922152b45357a49ee6b310e9e0ecf38bd3955`](https://github.com/mobxjs/mobx/commit/f1f922152b45357a49ee6b310e9e0ecf38bd3955) [#3921](https://github.com/mobxjs/mobx/pull/3921) Thanks [@urugator](https://github.com/urugator)! - fix: #3919 new set methods not working with observable set
+
+## 6.13.1
+
+### Patch Changes
+
+- [`5e711e0b4737fd6b5b3c6f9b32afd4f195bc5fc3`](https://github.com/mobxjs/mobx/commit/5e711e0b4737fd6b5b3c6f9b32afd4f195bc5fc3) [#3901](https://github.com/mobxjs/mobx/pull/3901) Thanks [@peterm-canva](https://github.com/peterm-canva)! - Shrink Atom and Reaction using a bitfield
+
+## 6.13.0
+
+### Minor Changes
+
+- [`16f070e6aac60e9010c2591b1743276d700b23d5`](https://github.com/mobxjs/mobx/commit/16f070e6aac60e9010c2591b1743276d700b23d5) [#3898](https://github.com/mobxjs/mobx/pull/3898) Thanks [@inoyakaigor](https://github.com/inoyakaigor)! - Added new Set methods
+
+## 6.12.5
+
+### Patch Changes
+
+- [`ba890343`](https://github.com/mobxjs/mobx/commit/ba8903430ce96746db5dcde6b78edeb195ea8018) [#3893](https://github.com/mobxjs/mobx/pull/3893) Thanks [@g6123](https://github.com/g6123)! - Fix ES6 Map/Set checks for cross-window scripts
+
+## 6.12.4
+
+### Patch Changes
+
+- [`e9e1955f`](https://github.com/mobxjs/mobx/commit/e9e1955f745545d796d906b6e0ba04a6cde3f1ee) [#3880](https://github.com/mobxjs/mobx/pull/3880) Thanks [@peterm-canva](https://github.com/peterm-canva)! - Shrink ComputedValue using a bitfield
+
+## 6.12.2
+
+### Patch Changes
+
+- [`61abc53f`](https://github.com/mobxjs/mobx/commit/61abc53ff10554d1d5ce3e85466f6beda4d63fa2) [#3852](https://github.com/mobxjs/mobx/pull/3852) Thanks [@mweststrate](https://github.com/mweststrate)! - Patched the release process, forcing release to get everything in pristine state.
+
+* [`b28e0ebb`](https://github.com/mobxjs/mobx/commit/b28e0ebbfc9aa11293bc185216da92997e497fd3) [#3816](https://github.com/mobxjs/mobx/pull/3816) Thanks [@barroij](https://github.com/barroij)! - Fix `IReactionDisposer` and `IIsObservableObject` interface definition so that Typescript knows the property key `$mobx` is a symbol and not a string
+
+## 6.12.1
+
+### Patch Changes
+
+- [`620f78c7`](https://github.com/mobxjs/mobx/commit/620f78c74e66bc532a96e28b26fd2d0ed1b67d54) [#3812](https://github.com/mobxjs/mobx/pull/3812) Thanks [@barroij](https://github.com/barroij)! - Prevent `reaction` from heeping a Reference to the OldValue that would prevent GC.
+
+* [`6111b093`](https://github.com/mobxjs/mobx/commit/6111b0939d0d3c0d46dc325ba6bbd5f740a161d3) [#3833](https://github.com/mobxjs/mobx/pull/3833) Thanks [@realyze](https://github.com/realyze)! - Reduce memory overhead of tracking dependencies
+
+## 6.12.0
+
+### Minor Changes
+
+- [`ec5db592`](https://github.com/mobxjs/mobx/commit/ec5db592d7756826c31e710b1c759d7e9406b153) [#3792](https://github.com/mobxjs/mobx/pull/3792) Thanks [@tonyraoul](https://github.com/tonyraoul)! - Improve observablearray proxy pefromance for es2023.array and es2022.array methods
+
+### Patch Changes
+
+- [`86616c11`](https://github.com/mobxjs/mobx/commit/86616c11c108a511331eb05e55c08fc2c5a23f4d) [#3654](https://github.com/mobxjs/mobx/pull/3654) Thanks [@ahoisl](https://github.com/ahoisl)! - fix: action transparently forwards toString of underlying function
+
+## 6.11.0
+
+### Minor Changes
+
+- [`c9260974`](https://github.com/mobxjs/mobx/commit/c9260974f726f58de0fd4974ea024c644d9b7c6f) [#3790](https://github.com/mobxjs/mobx/pull/3790) Thanks [@mweststrate](https://github.com/mweststrate)! - Added support for modern 2022.3 Decorators. [#3790](https://github.com/mobxjs/mobx/pull/3790)
+ - [Installation / usage instruction](https://mobx.js.org/enabling-decorators.html).
+ - [Introduction announcement](https://michel.codes/blogs/mobx-decorators)
+ - Original PR by [@Matchlighter](https://github.com/Matchlighter) in [#3638](https://github.com/mobxjs/mobx/pull/3638),
+
+## 6.10.2
+
+### Patch Changes
+
+- [`c8d9374d`](https://github.com/mobxjs/mobx/commit/c8d9374d4f3b05cfec0d690e0eb3ada4f619ff0b) [#3748](https://github.com/mobxjs/mobx/pull/3748) Thanks [@mweststrate](https://github.com/mweststrate)! - Fixed: #3747, computed values becoming stale if the underlying observable was created and updated outside a reactive context
+
+## 6.10.1
+
+### Patch Changes
+
+- [`3ceeb865`](https://github.com/mobxjs/mobx/commit/3ceeb8651e328c4c7211c875696b3f5269fea834) [#3732](https://github.com/mobxjs/mobx/pull/3732) Thanks [@urugator](https://github.com/urugator)! - - fix: #3728: Observable initialization updates state version.
+ - fix: Observable set initialization violates `enforceActions: "always"`.
+ - fix: Changing keys of observable object does not respect `enforceActions`.
+
+## 6.10.0
+
+### Minor Changes
+
+- [`bebd5f05`](https://github.com/mobxjs/mobx/commit/bebd5f0507a109145f401c78630ed9d59e4a1101) [#3727](https://github.com/mobxjs/mobx/pull/3727) Thanks [@rluvaton](https://github.com/rluvaton)! - Added support for `signal` (AbortSignal) in `autorun`, `reaction` and sync `when` options to dispose them
+
+### Patch Changes
+
+- [`55f78ddc`](https://github.com/mobxjs/mobx/commit/55f78ddc20e84f38a7aa88b99a51ad994e558241) [#3717](https://github.com/mobxjs/mobx/pull/3717) Thanks [@liucan233](https://github.com/liucan233)! - remove proxy option for makeObservable and makeAutoObservable
+
+## 6.9.1
+
+### Patch Changes
+
+- [`4792303e`](https://github.com/mobxjs/mobx/commit/4792303ec9119c1ba54134fff7e845d21a1d9337) [#3709](https://github.com/mobxjs/mobx/pull/3709) Thanks [@kubk](https://github.com/kubk)! - Make trace() noop in production build
+
+## 6.9.0
+
+### Minor Changes
+
+- [`44a2cf42`](https://github.com/mobxjs/mobx/commit/44a2cf42dec7635f639ddbfb19202ebc710bac54) [#3590](https://github.com/mobxjs/mobx/pull/3590) Thanks [@urugator](https://github.com/urugator)! - Better support for React 18: Mobx now keeps track of a global state version, which updates with each mutation.
+
+## 6.8.0
+
+### Minor Changes
+
+- [`fed3ff14`](https://github.com/mobxjs/mobx/commit/fed3ff14ca4dcbc788c4678e6d3f4edf747ffdb0) [#3608](https://github.com/mobxjs/mobx/pull/3608) Thanks [@emereum](https://github.com/emereum)! - Do not expose the methods `observe_` or `intercept_` on computed values and observable values.
+
+### Patch Changes
+
+- [`42f8ac05`](https://github.com/mobxjs/mobx/commit/42f8ac057ec70c508232339016cc7249123f0fd0) [#3596](https://github.com/mobxjs/mobx/pull/3596) Thanks [@urugator](https://github.com/urugator)! - fix #3595 onBecomeObserved is not called for ObservableSet
+
+* [`2bccc5b3`](https://github.com/mobxjs/mobx/commit/2bccc5b3ca1df6444c942c715718519d590281e0) [#3583](https://github.com/mobxjs/mobx/pull/3583) Thanks [@urugator](https://github.com/urugator)! - fix #3582: AbortSignal leaks @types/node
+
+- [`7095fa45`](https://github.com/mobxjs/mobx/commit/7095fa4569afb538b7f153ce2b2a8078f2dbe1fc) [#3609](https://github.com/mobxjs/mobx/pull/3609) Thanks [@emereum](https://github.com/emereum)! - Restore generic types for newValue and oldValue on IValueDidChange and IBoxDidChange.
+
+## 6.7.0
+
+### Minor Changes
+
+- [`8cf4784f`](https://github.com/mobxjs/mobx/commit/8cf4784f53857cc977aed641bd778f2c14a080f5) [#3559](https://github.com/mobxjs/mobx/pull/3559) Thanks [@urugator](https://github.com/urugator)! - Proxied observable arrays can now safely read/write out of bound indices. See https://github.com/mobxjs/mobx/discussions/3537
+
+* [`223e3688`](https://github.com/mobxjs/mobx/commit/223e3688631528a327c79d39e2f497c6e1506165) [#3551](https://github.com/mobxjs/mobx/pull/3551) Thanks [@deadbeef84](https://github.com/deadbeef84)! - Added new option `signal` to `when()`, to support abortion using an AbortSignal / AbortController.
+
+### Patch Changes
+
+- [`fe25cfed`](https://github.com/mobxjs/mobx/commit/fe25cfede0aee3bddd7fa434a14ed4b40a57ee26) [#3566](https://github.com/mobxjs/mobx/pull/3566) Thanks [@upsuper](https://github.com/upsuper)! - Make return value of reportObserved match invoke of onBecomeObserved
+
+## 6.6.2
+
+### Patch Changes
+
+- [`b375535c`](https://github.com/mobxjs/mobx/commit/b375535c422453963f5d3485a2ef5233568c12a6) [#3344](https://github.com/mobxjs/mobx/pull/3344) Thanks [@Nokel81](https://github.com/Nokel81)! - Allow readonly tuples as part of IObservableMapInitialValues
+
+* [`7260cd41`](https://github.com/mobxjs/mobx/commit/7260cd413b1e52449523826ac239c2a197b2880f) [#3516](https://github.com/mobxjs/mobx/pull/3516) Thanks [@urugator](https://github.com/urugator)! - fix regression #3514: LegacyObservableArray compat with Safari 9.\*
+
+- [`78d1aa23`](https://github.com/mobxjs/mobx/commit/78d1aa2362b4dc5d521518688d6ac7e2d4f7ad3a) [#3458](https://github.com/mobxjs/mobx/pull/3458) Thanks [@egilll](https://github.com/egilll)! - A slight revamp of the README, wording, and clearer links
+
+## 6.6.1
+
+### Patch Changes
+
+- [`63698d06`](https://github.com/mobxjs/mobx/commit/63698d0681988194bac5fc01851882b417b35f18) [#3427](https://github.com/mobxjs/mobx/pull/3427) Thanks [@RyanCavanaugh](https://github.com/RyanCavanaugh)! - Apply 'object' constraint where required
+
+## 6.6.0
+
+### Minor Changes
+
+- [`8e204c7b`](https://github.com/mobxjs/mobx/commit/8e204c7b7d1dbad597761fa83beda77f027ee34c) [#3409](https://github.com/mobxjs/mobx/pull/3409) Thanks [@Nokel81](https://github.com/Nokel81)! - Remove observable.box type inconsistancy
+
## 6.5.0
### Minor Changes
@@ -244,7 +447,7 @@ Support the ongoing maintenance at: https://opencollective.com/mobx
[`28f8a11d`](https://github.com/mobxjs/mobx/commit/28f8a11d8b94f1aca2eec4ae9c5f45c5ea2f4362) [#2641](https://github.com/mobxjs/mobx/pull/2641) Thanks [@urugator](https://github.com/urugator)!
-- `action`, `computed`, `flow` defined on prototype can be overriden by subclass via `override` annotation/decorator. Previously broken.
+- `action`, `computed`, `flow` defined on prototype can be overridden by subclass via `override` annotation/decorator. Previously broken.
- Overriding anything defined on instance itself (`this`) is not supported and should throw. Previously partially possible or broken.
- Attempt to re-annotate property always throws. Previously mostly undefined outcome.
- All annotated and non-observable props (action/flow) are non-writable. Previously writable.
@@ -1271,7 +1474,7 @@ A deprecation message will now be printed if creating computed properties while
```javascript
const x = observable({
- computedProp: function() {
+ computedProp: function () {
return someComputation
}
})
@@ -1296,7 +1499,7 @@ or alternatively:
```javascript
observable({
- computedProp: computed(function() {
+ computedProp: computed(function () {
return someComputation
})
})
@@ -1314,7 +1517,7 @@ N.B. If you want to introduce actions on an observable that modify its state, us
```javascript
observable({
counter: 0,
- increment: action(function() {
+ increment: action(function () {
this.counter++
})
})
@@ -1440,10 +1643,10 @@ function Square() {
extendObservable(this, {
length: 2,
squared: computed(
- function() {
+ function () {
return this.squared * this.squared
},
- function(surfaceSize) {
+ function (surfaceSize) {
this.length = Math.sqrt(surfaceSize)
}
)
@@ -1505,7 +1708,7 @@ function Square() {
- Fixed #360: Removed expensive cycle detection (cycles are still detected, but a bit later)
- Fixed #377: `toJS` serialization of Dates and Regexes preserves the original values
-- Fixed #379: `@action` decorated methods can now be inherited / overriden
+- Fixed #379: `@action` decorated methods can now be inherited / overridden
## 2.3.3
diff --git a/packages/mobx/__tests__/decorators_20223/stage3-decorators.ts b/packages/mobx/__tests__/decorators_20223/stage3-decorators.ts
new file mode 100644
index 0000000000..7b788ca681
--- /dev/null
+++ b/packages/mobx/__tests__/decorators_20223/stage3-decorators.ts
@@ -0,0 +1,1117 @@
+"use strict"
+
+import {
+ observe,
+ computed,
+ observable,
+ autorun,
+ extendObservable,
+ action,
+ IObservableArray,
+ IArrayWillChange,
+ IArrayWillSplice,
+ IObservableValue,
+ isObservable,
+ isObservableProp,
+ isObservableObject,
+ transaction,
+ IObjectDidChange,
+ spy,
+ configure,
+ isAction,
+ IAtom,
+ createAtom,
+ runInAction,
+ makeObservable
+} from "../../src/mobx"
+import { type ObservableArrayAdministration } from "../../src/internal"
+import * as mobx from "../../src/mobx"
+
+const testFunction = function (a: any) {}
+
+// lazy wrapper around yest
+
+const t = {
+ equal(a: any, b: any) {
+ expect(a).toBe(b)
+ },
+ deepEqual(a: any, b: any) {
+ expect(a).toEqual(b)
+ },
+ notEqual(a: any, b: any) {
+ expect(a).not.toEqual(b)
+ },
+
+ throws(a: any, b: any) {
+ expect(a).toThrow(b)
+ }
+}
+
+test("decorators", () => {
+ class Order {
+ @observable accessor price: number = 3
+ @observable accessor amount: number = 2
+ @observable accessor orders: string[] = []
+ @observable accessor aFunction = testFunction
+
+ @computed
+ get total() {
+ return this.amount * this.price * (1 + this.orders.length)
+ }
+ }
+
+ const o = new Order()
+ t.equal(isObservableObject(o), true)
+ t.equal(isObservableProp(o, "amount"), true)
+ t.equal(isObservableProp(o, "total"), true)
+
+ const events: any[] = []
+ const d1 = observe(o, (ev: IObjectDidChange) => events.push(ev.name, (ev as any).oldValue))
+ const d2 = observe(o, "price", ev => events.push(ev.newValue, ev.oldValue))
+ const d3 = observe(o, "total", ev => events.push(ev.newValue, ev.oldValue))
+
+ o.price = 4
+
+ d1()
+ d2()
+ d3()
+
+ o.price = 5
+
+ t.deepEqual(events, [
+ 8, // new total
+ 6, // old total
+ 4, // new price
+ 3, // old price
+ "price", // event name
+ 3 // event oldValue
+ ])
+})
+
+test("annotations", () => {
+ const fn0 = () => 0
+ class Order {
+ @observable accessor price: number = 3
+ @observable accessor amount: number = 2
+ @observable accessor orders: string[] = []
+ @observable accessor aFunction = fn0
+
+ @computed
+ get total() {
+ return this.amount * this.price * (1 + this.orders.length)
+ }
+ }
+
+ const order1totals: number[] = []
+ const order1 = new Order()
+ const order2 = new Order()
+
+ const disposer = autorun(() => {
+ order1totals.push(order1.total)
+ })
+
+ order2.price = 4
+ order1.amount = 1
+
+ t.equal(order1.price, 3)
+ t.equal(order1.total, 3)
+ t.equal(order2.total, 8)
+ order2.orders.push("bla")
+ t.equal(order2.total, 16)
+
+ order1.orders.splice(0, 0, "boe", "hoi")
+ t.deepEqual(order1totals, [6, 3, 9])
+
+ disposer()
+ order1.orders.pop()
+ t.equal(order1.total, 6)
+ t.deepEqual(order1totals, [6, 3, 9])
+ expect(isAction(order1.aFunction)).toBe(true)
+ expect(order1.aFunction()).toBe(0)
+ order1.aFunction = () => 1
+ expect(isAction(order1.aFunction)).toBe(true)
+ expect(order1.aFunction()).toBe(1)
+})
+
+test("box", () => {
+ class Box {
+ @observable accessor uninitialized: any
+ @observable accessor height = 20
+ @observable accessor sizes = [2]
+ @observable accessor someFunc = function () {
+ return 2
+ }
+ @computed
+ get width() {
+ return this.height * this.sizes.length * this.someFunc() * (this.uninitialized ? 2 : 1)
+ }
+ @action("test")
+ addSize() {
+ this.sizes.push(3)
+ this.sizes.push(4)
+ }
+ }
+
+ const box = new Box()
+
+ const ar: number[] = []
+
+ autorun(() => {
+ ar.push(box.width)
+ })
+
+ t.deepEqual(ar.slice(), [40])
+ box.height = 10
+ t.deepEqual(ar.slice(), [40, 20])
+ box.sizes.push(3, 4)
+ t.deepEqual(ar.slice(), [40, 20, 60])
+ box.someFunc = () => 7
+ t.deepEqual(ar.slice(), [40, 20, 60, 210])
+ box.uninitialized = true
+ t.deepEqual(ar.slice(), [40, 20, 60, 210, 420])
+ box.addSize()
+ expect(ar.slice()).toEqual([40, 20, 60, 210, 420, 700])
+})
+
+test("computed setter should succeed", () => {
+ class Bla {
+ @observable accessor a = 3
+ @computed
+ get propX() {
+ return this.a * 2
+ }
+ set propX(v) {
+ this.a = v
+ }
+ }
+
+ const b = new Bla()
+ t.equal(b.propX, 6)
+ b.propX = 4
+ t.equal(b.propX, 8)
+})
+
+test("ClassFieldDecorators should NOT work without accessor without legacy compilation", () => {
+ expect(() => {
+ class Order {
+ @observable price: number = 3
+ }
+ }).toThrow("[MobX] Please use `@observable accessor price` instead of `@observable price`")
+})
+
+test("Reasonable error for decorator kind mismatch", () => {
+ expect(() => {
+ class Order {
+ // @ts-ignore
+ @computed total = 3
+ }
+ }).toThrow("[MobX] The decorator applied to 'total' cannot be used on a field element")
+})
+
+test("typescript: parameterized computed decorator", () => {
+ class TestClass {
+ @observable accessor x = 3
+ @observable accessor y = 3
+ @computed.struct
+ get boxedSum() {
+ return { sum: Math.round(this.x) + Math.round(this.y) }
+ }
+ }
+
+ const t1 = new TestClass()
+ const changes: { sum: number }[] = []
+ const d = autorun(() => changes.push(t1.boxedSum))
+
+ t1.y = 4 // change
+ t.equal(changes.length, 2)
+ t1.y = 4.2 // no change
+ t.equal(changes.length, 2)
+ transaction(() => {
+ t1.y = 3
+ t1.x = 4
+ }) // no change
+ t.equal(changes.length, 2)
+ t1.x = 6 // change
+ t.equal(changes.length, 3)
+ d()
+
+ t.deepEqual(changes, [{ sum: 6 }, { sum: 7 }, { sum: 9 }])
+})
+
+test("issue 165", () => {
+ function report(msg: string, value: T) {
+ // console.log(msg, ":", value)
+ return value
+ }
+
+ class Card {
+ constructor(public game: Game, public id: number) {
+ makeObservable(this)
+ }
+
+ @computed
+ get isWrong() {
+ return report(
+ "Computing isWrong for card " + this.id,
+ this.isSelected && this.game.isMatchWrong
+ )
+ }
+
+ @computed
+ get isSelected() {
+ return report(
+ "Computing isSelected for card" + this.id,
+ this.game.firstCardSelected === this || this.game.secondCardSelected === this
+ )
+ }
+ }
+
+ class Game {
+ @observable accessor firstCardSelected: Card | null = null
+ @observable accessor secondCardSelected: Card | null = null
+
+ @computed
+ get isMatchWrong() {
+ return report(
+ "Computing isMatchWrong",
+ this.secondCardSelected !== null &&
+ this.firstCardSelected!.id !== this.secondCardSelected.id
+ )
+ }
+ }
+
+ let game = new Game()
+ let card1 = new Card(game, 1),
+ card2 = new Card(game, 2)
+
+ autorun(() => {
+ card1.isWrong
+ card2.isWrong
+ // console.log("card1.isWrong =", card1.isWrong)
+ // console.log("card2.isWrong =", card2.isWrong)
+ // console.log("------------------------------")
+ })
+
+ // console.log("Selecting first card")
+ game.firstCardSelected = card1
+ // console.log("Selecting second card")
+ game.secondCardSelected = card2
+
+ t.equal(card1.isWrong, true)
+ t.equal(card2.isWrong, true)
+})
+
+test("issue 191 - shared initializers (2022.3)", () => {
+ class Test {
+ @observable accessor obj = { a: 1 }
+ @observable accessor array = [2]
+ }
+
+ const t1 = new Test()
+ t1.obj.a = 2
+ t1.array.push(3)
+
+ const t2 = new Test()
+ t2.obj.a = 3
+ t2.array.push(4)
+
+ t.notEqual(t1.obj, t2.obj)
+ t.notEqual(t1.array, t2.array)
+ t.equal(t1.obj.a, 2)
+ t.equal(t2.obj.a, 3)
+
+ t.deepEqual(t1.array.slice(), [2, 3])
+ t.deepEqual(t2.array.slice(), [2, 4])
+})
+
+function normalizeSpyEvents(events: any[]) {
+ events.forEach(ev => {
+ delete ev.fn
+ delete ev.time
+ })
+ return events
+}
+
+test("action decorator (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {}
+
+ @action
+ add(a: number, b: number): number {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(3)
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(2, 2), 12)
+ t.equal(store1.add(1, 1), 4)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ { arguments: [3, 4], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [2, 2], name: "add", spyReportStart: true, object: store2, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [1, 1], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("custom action decorator (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {}
+
+ @action("zoem zoem")
+ add(a: number, b: number): number {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(3)
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(2, 2), 12)
+ t.equal(store1.add(1, 1), 4)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ {
+ arguments: [3, 4],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [2, 2],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store2,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [1, 1],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("action decorator on field (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {}
+
+ @action
+ add = (a: number, b: number) => {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(7)
+ expect(store1.add).not.toEqual(store2.add)
+
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(4, 5), 63)
+ t.equal(store1.add(2, 2), 8)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ { arguments: [3, 4], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [4, 5], name: "add", spyReportStart: true, object: store2, type: "action" },
+ { type: "report-end", spyReportEnd: true },
+ { arguments: [2, 2], name: "add", spyReportStart: true, object: store1, type: "action" },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("custom action decorator on field (2022.3)", () => {
+ class Store {
+ constructor(private multiplier: number) {}
+
+ @action("zoem zoem")
+ add = (a: number, b: number) => {
+ return (a + b) * this.multiplier
+ }
+ }
+
+ const store1 = new Store(2)
+ const store2 = new Store(7)
+
+ const events: any[] = []
+ const d = spy(events.push.bind(events))
+ t.equal(store1.add(3, 4), 14)
+ t.equal(store2.add(4, 5), 63)
+ t.equal(store1.add(2, 2), 8)
+
+ t.deepEqual(normalizeSpyEvents(events), [
+ {
+ arguments: [3, 4],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [4, 5],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store2,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true },
+ {
+ arguments: [2, 2],
+ name: "zoem zoem",
+ spyReportStart: true,
+ object: store1,
+ type: "action"
+ },
+ { type: "report-end", spyReportEnd: true }
+ ])
+
+ d()
+})
+
+test("267 (2022.3) should be possible to declare properties observable outside strict mode", () => {
+ configure({ enforceActions: "observed" })
+
+ class Store {
+ @observable accessor timer: number | null = null
+ }
+
+ configure({ enforceActions: "never" })
+})
+
+test("288 atom not detected for object property", () => {
+ class Store {
+ @observable accessor foo = ""
+ }
+
+ const store = new Store()
+
+ mobx.observe(
+ store,
+ "foo",
+ () => {
+ // console.log("Change observed")
+ },
+ true
+ )
+})
+
+test.skip("observable performance - ts - decorators", () => {
+ const AMOUNT = 100000
+
+ class A {
+ @observable accessor a = 1
+ @observable accessor b = 2
+ @observable accessor c = 3
+ @computed
+ get d() {
+ return this.a + this.b + this.c
+ }
+ }
+
+ const objs: any[] = []
+ const start = Date.now()
+
+ for (let i = 0; i < AMOUNT; i++) objs.push(new A())
+
+ console.log("created in ", Date.now() - start)
+
+ for (let j = 0; j < 4; j++) {
+ for (let i = 0; i < AMOUNT; i++) {
+ const obj = objs[i]
+ obj.a += 3
+ obj.b *= 4
+ obj.c = obj.b - obj.a
+ obj.d
+ }
+ }
+
+ console.log("changed in ", Date.now() - start)
+})
+
+test("unbound methods", () => {
+ class A {
+ // shared across all instances
+ @action
+ m1() {}
+ }
+
+ const a1 = new A()
+ const a2 = new A()
+
+ t.equal(a1.m1, a2.m1)
+ t.equal(Object.hasOwnProperty.call(a1, "m1"), false)
+ t.equal(Object.hasOwnProperty.call(a2, "m1"), false)
+})
+
+test("inheritance", () => {
+ class A {
+ @observable accessor a = 2
+ }
+
+ class B extends A {
+ @observable accessor b = 3
+ @computed
+ get c() {
+ return this.a + this.b
+ }
+ constructor() {
+ super()
+ makeObservable(this)
+ }
+ }
+ const b1 = new B()
+ const b2 = new B()
+ const values: any[] = []
+ mobx.autorun(() => values.push(b1.c + b2.c))
+
+ b1.a = 3
+ b1.b = 4
+ b2.b = 5
+ b2.a = 6
+
+ t.deepEqual(values, [10, 11, 12, 14, 18])
+})
+
+test("inheritance overrides observable", () => {
+ class A {
+ @observable accessor a = 2
+ }
+
+ class B {
+ @observable accessor a = 5
+ @observable accessor b = 3
+ @computed
+ get c() {
+ return this.a + this.b
+ }
+ }
+
+ const b1 = new B()
+ const b2 = new B()
+ const values: any[] = []
+ mobx.autorun(() => values.push(b1.c + b2.c))
+
+ b1.a = 3
+ b1.b = 4
+ b2.b = 5
+ b2.a = 6
+
+ t.deepEqual(values, [16, 14, 15, 17, 18])
+})
+
+test("reusing initializers", () => {
+ class A {
+ @observable accessor a = 3
+ @observable accessor b = this.a + 2
+ @computed
+ get c() {
+ return this.a + this.b
+ }
+ @computed
+ get d() {
+ return this.c + 1
+ }
+ }
+
+ const a = new A()
+ const values: any[] = []
+ mobx.autorun(() => values.push(a.d))
+
+ a.a = 4
+ t.deepEqual(values, [9, 10])
+})
+
+test("enumerability", () => {
+ class A {
+ @observable accessor a = 1 // enumerable, on proto
+ @computed
+ get b() {
+ return this.a
+ } // non-enumerable, (and, ideally, on proto)
+ @action
+ m() {} // non-enumerable, on proto
+ }
+
+ const a = new A()
+
+ // not initialized yet
+ let ownProps = Object.keys(a)
+ let enumProps: string[] = []
+ for (const key in a) enumProps.push(key)
+
+ t.deepEqual(ownProps, [])
+
+ t.deepEqual(enumProps, [])
+
+ t.equal("a" in a, true)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("a"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("b"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("m"), false)
+
+ t.equal(mobx.isAction(a.m), true)
+
+ // after initialization
+ a.a
+ a.b
+ a.m
+
+ ownProps = Object.keys(a)
+ enumProps = []
+ for (const key in a) enumProps.push(key)
+
+ t.deepEqual(ownProps, [])
+
+ t.deepEqual(enumProps, [])
+
+ t.equal("a" in a, true)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("a"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("b"), false)
+ // eslint-disable-next-line
+ t.equal(a.hasOwnProperty("m"), false)
+})
+
+// Re-enable when late initialization is supported in TS
+test.skip("issue 285 (2022.3)", () => {
+ const { observable, toJS } = mobx
+
+ class Todo {
+ id = 1
+ @observable accessor title: string
+ @observable accessor finished = false
+ @observable accessor childThings = [1, 2, 3]
+ @computed get bla() {
+ return 3
+ }
+ @action someMethod() {}
+ constructor(title: string) {
+ this.title = title
+ }
+ }
+
+ const todo = new Todo("Something to do")
+
+ t.deepEqual(toJS(todo), {
+ id: 1,
+ title: "Something to do",
+ finished: false,
+ childThings: [1, 2, 3]
+ })
+})
+
+// Re-enable when late initialization is supported in TS
+test.skip("verify object assign (2022.3) (legacy/field decorator)", () => {
+ class Todo {
+ @observable accessor title = "test"
+ @computed
+ get upperCase() {
+ return this.title.toUpperCase()
+ }
+ }
+
+ t.deepEqual((Object as any).assign({}, new Todo()), {
+ title: "test"
+ })
+})
+
+test("373 - fix isObservable for unused computed", () => {
+ class Bla {
+ ts_53332_workaround: string = ""
+
+ @computed
+ get computedVal() {
+ return 3
+ }
+ constructor() {
+ makeObservable(this)
+ t.equal(isObservableProp(this, "computedVal"), true)
+ this.computedVal
+ t.equal(isObservableProp(this, "computedVal"), true)
+ }
+ }
+
+ new Bla()
+})
+
+test("705 - setter undoing caching (2022.3)", () => {
+ let recomputes = 0
+ let autoruns = 0
+
+ class Person {
+ @observable accessor name: string = ""
+ @observable accessor title: string = ""
+
+ // Typescript bug: if fullName is before the getter, the property is defined twice / incorrectly, see #705
+ // set fullName(val) {
+ // // Noop
+ // }
+ @computed
+ get fullName() {
+ recomputes++
+ return this.title + " " + this.name
+ }
+ // Should also be possible to define the setter _before_ the fullname
+ set fullName(val) {
+ // Noop
+ }
+ }
+
+ let p1 = new Person()
+ p1.name = "Tom Tank"
+ p1.title = "Mr."
+
+ t.equal(recomputes, 0)
+ t.equal(autoruns, 0)
+
+ const d1 = autorun(() => {
+ autoruns++
+ p1.fullName
+ })
+
+ const d2 = autorun(() => {
+ autoruns++
+ p1.fullName
+ })
+
+ t.equal(recomputes, 1)
+ t.equal(autoruns, 2)
+
+ p1.title = "Master"
+ t.equal(recomputes, 2)
+ t.equal(autoruns, 4)
+
+ d1()
+ d2()
+})
+
+test("@observable.ref (2022.3)", () => {
+ class A {
+ @observable.ref accessor ref = { a: 3 }
+ }
+
+ const a = new A()
+ t.equal(a.ref.a, 3)
+ t.equal(mobx.isObservable(a.ref), false)
+ t.equal(mobx.isObservableProp(a, "ref"), true)
+})
+
+test("@observable.shallow (2022.3)", () => {
+ class A {
+ @observable.shallow accessor arr = [{ todo: 1 }]
+ }
+
+ const a = new A()
+ const todo2 = { todo: 2 }
+ a.arr.push(todo2)
+ t.equal(mobx.isObservable(a.arr), true)
+ t.equal(mobx.isObservableProp(a, "arr"), true)
+ t.equal(mobx.isObservable(a.arr[0]), false)
+ t.equal(mobx.isObservable(a.arr[1]), false)
+ t.equal(a.arr[1] === todo2, true)
+})
+
+test("@observable.shallow - 2 (2022.3)", () => {
+ class A {
+ @observable.shallow accessor arr: Record = { x: { todo: 1 } }
+ }
+
+ const a = new A()
+ const todo2 = { todo: 2 }
+ a.arr.y = todo2
+ t.equal(mobx.isObservable(a.arr), true)
+ t.equal(mobx.isObservableProp(a, "arr"), true)
+ t.equal(mobx.isObservable(a.arr.x), false)
+ t.equal(mobx.isObservable(a.arr.y), false)
+ t.equal(a.arr.y === todo2, true)
+})
+
+test("@observable.deep (2022.3)", () => {
+ class A {
+ @observable.deep accessor arr = [{ todo: 1 }]
+ }
+
+ const a = new A()
+ const todo2 = { todo: 2 }
+ a.arr.push(todo2)
+
+ t.equal(mobx.isObservable(a.arr), true)
+ t.equal(mobx.isObservableProp(a, "arr"), true)
+ t.equal(mobx.isObservable(a.arr[0]), true)
+ t.equal(mobx.isObservable(a.arr[1]), true)
+ t.equal(a.arr[1] !== todo2, true)
+ t.equal(isObservable(todo2), false)
+})
+
+test("action.bound binds (2022.3)", () => {
+ class A {
+ @observable accessor x = 0
+ @action.bound
+ inc(value: number) {
+ this.x += value
+ }
+ }
+
+ const a = new A()
+ const runner = a.inc
+ runner(2)
+
+ t.equal(a.x, 2)
+})
+
+test("action.bound binds property function (2022.3)", () => {
+ class A {
+ @observable accessor x = 0
+ @action.bound
+ inc = function (value: number) {
+ this.x += value
+ }
+ }
+
+ const a = new A()
+ const runner = a.inc
+ runner(2)
+
+ t.equal(a.x, 2)
+})
+
+test("@computed.equals (2022.3)", () => {
+ const sameTime = (from: Time, to: Time) => from.hour === to.hour && from.minute === to.minute
+ class Time {
+ constructor(hour: number, minute: number) {
+ makeObservable(this)
+ this.hour = hour
+ this.minute = minute
+ }
+
+ @observable public accessor hour: number
+ @observable public accessor minute: number
+
+ @computed({ equals: sameTime })
+ public get time() {
+ return { hour: this.hour, minute: this.minute }
+ }
+ }
+ const time = new Time(9, 0)
+
+ const changes: Array<{ hour: number; minute: number }> = []
+ const disposeAutorun = autorun(() => changes.push(time.time))
+
+ t.deepEqual(changes, [{ hour: 9, minute: 0 }])
+ time.hour = 9
+ t.deepEqual(changes, [{ hour: 9, minute: 0 }])
+ time.minute = 0
+ t.deepEqual(changes, [{ hour: 9, minute: 0 }])
+ time.hour = 10
+ t.deepEqual(changes, [
+ { hour: 9, minute: 0 },
+ { hour: 10, minute: 0 }
+ ])
+ time.minute = 30
+ t.deepEqual(changes, [
+ { hour: 9, minute: 0 },
+ { hour: 10, minute: 0 },
+ { hour: 10, minute: 30 }
+ ])
+
+ disposeAutorun()
+})
+
+test("1072 - @observable accessor without initial value and observe before first access", () => {
+ class User {
+ @observable accessor loginCount: number = 0
+ }
+
+ const user = new User()
+ observe(user, "loginCount", () => {})
+})
+
+test("unobserved computed reads should warn with requiresReaction enabled", () => {
+ const consoleWarn = console.warn
+ const warnings: string[] = []
+ console.warn = function (...args) {
+ warnings.push(...args)
+ }
+ try {
+ class A {
+ @observable accessor x = 0
+
+ @computed({ requiresReaction: true })
+ get y() {
+ return this.x * 2
+ }
+ }
+
+ const a = new A()
+
+ a.y
+ const d = mobx.reaction(
+ () => a.y,
+ () => {}
+ )
+ a.y
+ d()
+ a.y
+
+ expect(warnings.length).toEqual(2)
+ expect(warnings[0]).toContain(
+ "is being read outside a reactive context. Doing a full recompute."
+ )
+ expect(warnings[1]).toContain(
+ "is being read outside a reactive context. Doing a full recompute."
+ )
+ } finally {
+ console.warn = consoleWarn
+ }
+})
+
+test("multiple inheritance should work", () => {
+ class A {
+ @observable accessor x = 1
+ }
+
+ class B extends A {
+ @observable accessor y = 1
+
+ constructor() {
+ super()
+ makeObservable(this)
+ }
+ }
+
+ const obsvKeys = [
+ ...(mobx._getAdministration(new B()) as ObservableArrayAdministration).values_.keys()
+ ]
+ expect(obsvKeys).toEqual(["x", "y"])
+})
+
+// 19.12.2020 @urugator:
+// All annotated non-observable fields are not writable.
+// All annotated fields of non-plain objects are non-configurable.
+// https://github.com/mobxjs/mobx/pull/2641
+test.skip("actions are reassignable", () => {
+ // See #1398 and #1545, make actions reassignable to support stubbing
+ class A {
+ @action
+ m1() {}
+ @action.bound
+ m3() {}
+ }
+
+ const a = new A()
+ expect(isAction(a.m1)).toBe(true)
+ expect(isAction(a.m3)).toBe(true)
+ a.m1 = () => {}
+ expect(isAction(a.m1)).toBe(false)
+ a.m3 = () => {}
+ expect(isAction(a.m3)).toBe(false)
+})
+
+test("it should support asyncAction as decorator (2022.3)", async () => {
+ mobx.configure({ enforceActions: "observed" })
+
+ class X {
+ @observable accessor a = 1
+
+ f = mobx.flow(function* f(this: X, initial: number) {
+ this.a = initial // this runs in action
+ this.a += yield Promise.resolve(5) as any
+ this.a = this.a * 2
+ return this.a
+ })
+ }
+
+ const x = new X()
+
+ expect(await x.f(3)).toBe(16)
+})
+
+test("toJS bug #1413 (2022.3)", () => {
+ class X {
+ @observable
+ accessor test = {
+ test1: 1
+ }
+ }
+
+ const x = new X()
+ const res = mobx.toJS(x.test) as any
+ expect(res).toEqual({ test1: 1 })
+ expect(res.__mobxDidRunLazyInitializers).toBe(undefined)
+})
+
+test("#2159 - computed property keys", () => {
+ const testSymbol = Symbol("test symbol")
+ const testString = "testString"
+
+ class TestClass {
+ @observable accessor [testSymbol] = "original symbol value"
+ @observable accessor [testString] = "original string value"
+ }
+
+ const o = new TestClass()
+
+ const events: any[] = []
+ observe(o, testSymbol, ev => events.push(ev.newValue, ev.oldValue))
+ observe(o, testString, ev => events.push(ev.newValue, ev.oldValue))
+
+ runInAction(() => {
+ o[testSymbol] = "new symbol value"
+ o[testString] = "new string value"
+ })
+
+ t.deepEqual(events, [
+ "new symbol value", // new symbol
+ "original symbol value", // original symbol
+ "new string value", // new string
+ "original string value" // original string
+ ])
+})
+
+test(`decorated field can be inherited, but doesn't inherite the effect of decorator`, () => {
+ class Store {
+ @action
+ action = () => {
+ return
+ }
+ }
+
+ class SubStore extends Store {
+ action = () => {
+ // should not be a MobX action
+ return
+ }
+ }
+
+ const store = new Store()
+ expect(isAction(store.action)).toBe(true)
+
+ const subStore = new SubStore()
+ expect(isAction(subStore.action)).toBe(false)
+})
diff --git a/packages/mobx/__tests__/decorators_20223/tsconfig.json b/packages/mobx/__tests__/decorators_20223/tsconfig.json
new file mode 100644
index 0000000000..31758eb645
--- /dev/null
+++ b/packages/mobx/__tests__/decorators_20223/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": ["../../tsconfig.json", "../../../../tsconfig.test.json"],
+ "compilerOptions": {
+ "target": "ES6",
+ "experimentalDecorators": false,
+ "useDefineForClassFields": true,
+
+ "rootDir": "../../"
+ },
+ "exclude": ["__tests__"],
+ "include": ["./", "../../src"] // ["../../src", "./"]
+}
diff --git a/packages/mobx/__tests__/perf/perf.js b/packages/mobx/__tests__/perf/perf.js
index 4bdcbc635c..419a961f6d 100644
--- a/packages/mobx/__tests__/perf/perf.js
+++ b/packages/mobx/__tests__/perf/perf.js
@@ -196,6 +196,39 @@ results of this test:
t.end()
})
+ test(`${version} - array.es2023 findLastIndex methods`, function (t) {
+ gc()
+ let aCalc = 0
+ let bCalc = 0
+ const ar = observable([0])
+ const findLastIndexOfZero = computed(function () {
+ aCalc++
+ return ar.findLastIndex(x => x === 0)
+ })
+ const lastIndexOfZero = computed(function () {
+ bCalc++
+ return ar.lastIndexOf(0)
+ })
+ mobx.observe(findLastIndexOfZero, voidObserver, true)
+ mobx.observe(lastIndexOfZero, voidObserver, true)
+
+ const start = now()
+
+ t.equal(1, aCalc)
+ t.equal(1, bCalc)
+ for (let i = 1; i < 10000; i++) ar.push(i)
+
+ t.equal(0, lastIndexOfZero.get())
+ t.equal(0, findLastIndexOfZero.get())
+ t.equal(10000, aCalc)
+ t.equal(10000, bCalc)
+
+ const end = now()
+
+ log("Array findLastIndex loop - Updated in " + (end - start) + " ms.")
+ t.end()
+ })
+
test(`${version} - array reduce`, function (t) {
gc()
let aCalc = 0
@@ -517,6 +550,146 @@ results of this test:
t.end()
})
+ test(`${version} - Set: initializing`, function (t) {
+ gc()
+ const iterationsCount = 100000
+ let i
+
+ const start = Date.now()
+ for (i = 0; i < iterationsCount; i++) {
+ mobx.observable.set()
+ }
+ const end = Date.now()
+ log("Initilizing " + iterationsCount + " maps: " + (end - start) + " ms.")
+ t.end()
+ })
+
+ test(`${version} - Set: setting and deleting properties`, function (t) {
+ gc()
+ const iterationsCount = 1000
+ const propertiesCount = 10000
+ const set = mobx.observable.set()
+ let i
+ let p
+
+ const start = Date.now()
+ for (i = 0; i < iterationsCount; i++) {
+ for (p = 0; p < propertiesCount; p++) {
+ set.add("" + p)
+ }
+ for (p = 0; p < propertiesCount; p++) {
+ set.delete("" + p)
+ }
+ }
+ const end = Date.now()
+
+ log(
+ "Setting and deleting " +
+ propertiesCount +
+ " set properties " +
+ iterationsCount +
+ " times: " +
+ (end - start) +
+ " ms."
+ )
+ t.end()
+ })
+
+ test(`${version} - Set: looking up properties`, function (t) {
+ gc()
+ const iterationsCount = 1000
+ const propertiesCount = 10000
+ const set = mobx.observable.set()
+ let i
+ let p
+
+ for (p = 0; p < propertiesCount; p++) {
+ set.add("" + p)
+ }
+
+ const start = Date.now()
+ for (i = 0; i < iterationsCount; i++) {
+ for (p = 0; p < propertiesCount; p++) {
+ set.has("" + p)
+ }
+ }
+ const end = Date.now()
+
+ log(
+ "Looking up " +
+ propertiesCount +
+ " set properties " +
+ iterationsCount +
+ " times: " +
+ (end - start) +
+ " ms."
+ )
+ t.end()
+ })
+
+ test(`${version} - Set: iterator helpers`, function (t) {
+ gc()
+ const iterationsCount = 1000
+ const propertiesCount = 10000
+ const set = mobx.observable.set()
+ let i
+ let p
+
+ for (p = 0; p < propertiesCount; p++) {
+ set.add("" + p)
+ }
+
+ const start = Date.now()
+ for (i = 0; i < iterationsCount; i++) {
+ set.entries().take(1)
+ }
+ const end = Date.now()
+
+ log(
+ "Single take out of" +
+ propertiesCount +
+ " set properties " +
+ iterationsCount +
+ " times: " +
+ (end - start) +
+ " ms."
+ )
+ t.end()
+ })
+
+ test(`${version} - Set: conversion to array`, function (t) {
+ gc()
+ const iterationsCount = 1000
+ const propertiesCount = 10000
+ const set = mobx.observable.set()
+ let i
+ let p
+
+ for (p = 0; p < propertiesCount; p++) {
+ set.add("" + p)
+ }
+
+ const start = Date.now()
+ for (i = 0; i < iterationsCount; i++) {
+ Array.from(set.keys())
+ Array.from(set.values())
+ Array.from(set.entries())
+ ;[...set]
+ }
+ const end = Date.now()
+
+ log(
+ "Converting " +
+ propertiesCount +
+ " set properties into an array" +
+ iterationsCount +
+ " times: " +
+ (end - start) +
+ " ms."
+ )
+ t.end()
+ })
+
test(`${version} - Map: initializing`, function (t) {
gc()
const iterationsCount = 100000
diff --git a/packages/mobx/__tests__/tsconfig.json b/packages/mobx/__tests__/tsconfig.json
new file mode 100644
index 0000000000..ecae17017d
--- /dev/null
+++ b/packages/mobx/__tests__/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../../tsconfig.test.json"
+}
diff --git a/packages/mobx/__tests__/v4/base/__snapshots__/array.js.snap b/packages/mobx/__tests__/v4/base/__snapshots__/array.js.snap
new file mode 100644
index 0000000000..36af5050d5
--- /dev/null
+++ b/packages/mobx/__tests__/v4/base/__snapshots__/array.js.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`very long arrays can be safely passed to nativeArray.concat #2379 1`] = `
+[MockFunction] {
+ "calls": [
+ [
+ "[mobx.array] Attempt to read an array index (10000) that is out of bounds (10000). Please check length first. Out of bound indices will not be tracked by MobX",
+ ],
+ ],
+ "results": [
+ {
+ "type": "return",
+ "value": undefined,
+ },
+ ],
+}
+`;
diff --git a/packages/mobx/__tests__/v4/base/__snapshots__/extras.js.snap b/packages/mobx/__tests__/v4/base/__snapshots__/extras.js.snap
index 140c429847..a530a78d50 100644
--- a/packages/mobx/__tests__/v4/base/__snapshots__/extras.js.snap
+++ b/packages/mobx/__tests__/v4/base/__snapshots__/extras.js.snap
@@ -1,8 +1,8 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`spy 1 1`] = `
-Array [
- Object {
+[
+ {
"debugObjectName": "ObservableValue@6",
"newValue": 4,
"observableKind": "value",
@@ -10,23 +10,23 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"debugObjectName": "ComputedValue@7",
"newValue": 8,
"observableKind": "computed",
"oldValue": 6,
"type": "update",
},
- Object {
+ {
"name": "Autorun@8",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
diff --git a/packages/mobx/__tests__/v4/base/__snapshots__/makereactive.js.snap b/packages/mobx/__tests__/v4/base/__snapshots__/makereactive.js.snap
index bc78201b69..8cb8eface6 100644
--- a/packages/mobx/__tests__/v4/base/__snapshots__/makereactive.js.snap
+++ b/packages/mobx/__tests__/v4/base/__snapshots__/makereactive.js.snap
@@ -1,3 +1,3 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`computed value 1`] = `"ComputedValue@1[() => 3]"`;
diff --git a/packages/mobx/__tests__/v4/base/__snapshots__/object-api.js.snap b/packages/mobx/__tests__/v4/base/__snapshots__/object-api.js.snap
index da9dd33d2a..1b67fb7d95 100644
--- a/packages/mobx/__tests__/v4/base/__snapshots__/object-api.js.snap
+++ b/packages/mobx/__tests__/v4/base/__snapshots__/object-api.js.snap
@@ -1,26 +1,26 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`observe & intercept 1`] = `
-Array [
- Object {
- "intercept": Object {
+[
+ {
+ "intercept": {
"name": "b",
- "newValue": Object {
+ "newValue": {
"title": "get tea",
},
- "object": Object {
- "a": Object {
+ "object": {
+ "a": {
"title": "get coffee",
},
},
"type": "add",
},
},
- Object {
- "intercept": Object {
+ {
+ "intercept": {
"name": "a",
- "object": Object {
- "a": Object {
+ "object": {
+ "a": {
"title": "get coffee",
},
},
@@ -31,16 +31,16 @@ Array [
`;
exports[`observe & intercept 2`] = `
-Array [
- Object {
- "observe": Object {
+[
+ {
+ "observe": {
"debugObjectName": "TestObject",
"name": "b",
- "newValue": Object {
+ "newValue": {
"title": "get tea",
},
- "object": Object {
- "b": Object {
+ "object": {
+ "b": {
"title": "get tea",
},
},
@@ -48,17 +48,17 @@ Array [
"type": "add",
},
},
- Object {
- "observe": Object {
+ {
+ "observe": {
"debugObjectName": "TestObject",
"name": "a",
- "object": Object {
- "b": Object {
+ "object": {
+ "b": {
"title": "get tea",
},
},
"observableKind": "object",
- "oldValue": Object {
+ "oldValue": {
"title": "get coffee",
},
"type": "remove",
diff --git a/packages/mobx/__tests__/v4/base/__snapshots__/observables.js.snap b/packages/mobx/__tests__/v4/base/__snapshots__/observables.js.snap
index 9d3551dee5..1fc3d3ed56 100644
--- a/packages/mobx/__tests__/v4/base/__snapshots__/observables.js.snap
+++ b/packages/mobx/__tests__/v4/base/__snapshots__/observables.js.snap
@@ -1,8 +1,8 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`delay autorun until end of transaction 1`] = `
-Array [
- Object {
+[
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 3,
@@ -11,11 +11,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 4,
@@ -24,12 +24,12 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"end1",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 5,
@@ -38,19 +38,19 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"end2",
- Object {
+ {
"name": "test",
"spyReportStart": true,
"type": "reaction",
},
"auto",
"calc y",
- Object {
+ {
"debugObjectName": "ObservableObject@1.b",
"newValue": 5,
"observableKind": "computed",
@@ -59,12 +59,12 @@ Array [
},
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"post trans1",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 6,
@@ -74,29 +74,29 @@ Array [
"type": "update",
},
"calc y",
- Object {
+ {
"debugObjectName": "ObservableObject@1.b",
"newValue": 6,
"observableKind": "computed",
"oldValue": 5,
"type": "update",
},
- Object {
+ {
"name": "test",
"spyReportStart": true,
"type": "reaction",
},
"auto",
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"post trans2",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 3,
@@ -105,7 +105,7 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
@@ -114,11 +114,11 @@ Array [
`;
exports[`issue 50 1`] = `
-Array [
+[
"auto",
"calc c",
"transstart",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": false,
@@ -127,11 +127,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "b",
"newValue": true,
@@ -140,26 +140,26 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"transpreend",
- Object {
+ {
"name": "ar",
"spyReportStart": true,
"type": "reaction",
},
"auto",
"calc c",
- Object {
+ {
"debugObjectName": "ObservableObject@1.c",
"newValue": true,
"observableKind": "computed",
"oldValue": false,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
@@ -168,11 +168,11 @@ Array [
`;
exports[`verify transaction events 1`] = `
-Array [
+[
"auto",
"calc c",
"transstart",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "b",
"newValue": 2,
@@ -181,26 +181,26 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"transpreend",
"calc c",
- Object {
+ {
"debugObjectName": "ObservableObject@1.c",
"newValue": 2,
"observableKind": "computed",
"oldValue": 1,
"type": "update",
},
- Object {
+ {
"name": "ar",
"spyReportStart": true,
"type": "reaction",
},
"auto",
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
diff --git a/packages/mobx/__tests__/v4/base/__snapshots__/spy.js.snap b/packages/mobx/__tests__/v4/base/__snapshots__/spy.js.snap
index bbdb70cc51..44ce6359af 100644
--- a/packages/mobx/__tests__/v4/base/__snapshots__/spy.js.snap
+++ b/packages/mobx/__tests__/v4/base/__snapshots__/spy.js.snap
@@ -1,13 +1,13 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`spy error 1`] = `
-Array [
- Object {
+[
+ {
"name": "autorun",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1.y",
"newValue": 4,
"observableKind": "computed",
@@ -16,11 +16,11 @@ Array [
},
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "x",
"newValue": 3,
@@ -29,7 +29,7 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1.y",
"newValue": CaughtException {
"cause": "Oops",
@@ -38,22 +38,22 @@ Array [
"oldValue": 4,
"type": "update",
},
- Object {
+ {
"name": "autorun",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"error": "Oops",
"message": "[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[autorun]'",
"name": "autorun",
"type": "error",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
@@ -61,14 +61,14 @@ Array [
`;
exports[`spy output 1`] = `
-Array [
- Object {
+[
+ {
"debugObjectName": "ObservableValue@1",
"newValue": "2",
"observableKind": "value",
"type": "create",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 3,
"observableKind": "value",
@@ -76,11 +76,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "c",
"newValue": 4,
@@ -88,11 +88,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "c",
"newValue": 5,
@@ -101,11 +101,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "d",
"newValue": 6,
@@ -113,11 +113,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "d",
"newValue": 7,
@@ -126,12 +126,12 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "added": Array [
+ {
+ "added": [
1,
2,
],
@@ -139,17 +139,17 @@ Array [
"debugObjectName": "ObservableArray@3",
"index": 0,
"observableKind": "array",
- "removed": Array [],
+ "removed": [],
"removedCount": 0,
"spyReportStart": true,
"type": "splice",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "added": Array [
+ {
+ "added": [
3,
4,
],
@@ -157,33 +157,33 @@ Array [
"debugObjectName": "ObservableArray@3",
"index": 2,
"observableKind": "array",
- "removed": Array [],
+ "removed": [],
"removedCount": 0,
"spyReportStart": true,
"type": "splice",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "added": Array [],
+ {
+ "added": [],
"addedCount": 0,
"debugObjectName": "ObservableArray@3",
"index": 0,
"observableKind": "array",
- "removed": Array [
+ "removed": [
1,
],
"removedCount": 1,
"spyReportStart": true,
"type": "splice",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableArray@3",
"index": 2,
"newValue": 5,
@@ -192,11 +192,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "g",
"newValue": 1,
@@ -204,11 +204,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "g",
"observableKind": "map",
@@ -216,11 +216,11 @@ Array [
"spyReportStart": true,
"type": "delete",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "i",
"newValue": 5,
@@ -228,11 +228,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "i",
"newValue": 6,
@@ -241,16 +241,16 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 6,
"observableKind": "computed",
@@ -259,11 +259,11 @@ Array [
},
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 4,
"observableKind": "value",
@@ -271,27 +271,27 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 8,
"observableKind": "computed",
"oldValue": 6,
"type": "update",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 5,
"observableKind": "value",
@@ -299,11 +299,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 6,
"observableKind": "value",
@@ -311,35 +311,35 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 12,
"observableKind": "computed",
"oldValue": 8,
"type": "update",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "arguments": Array [
+ {
+ "arguments": [
7,
],
"name": "myTestAction",
"spyReportStart": true,
"type": "action",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 7,
"observableKind": "value",
@@ -347,27 +347,27 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 14,
"observableKind": "computed",
"oldValue": 12,
"type": "update",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
diff --git a/packages/mobx/__tests__/v4/base/array.js b/packages/mobx/__tests__/v4/base/array.js
index 3721f569d6..955409527e 100644
--- a/packages/mobx/__tests__/v4/base/array.js
+++ b/packages/mobx/__tests__/v4/base/array.js
@@ -1,9 +1,24 @@
"use strict"
+const { LegacyObservableArray } = require("../../../src/internal")
const mobx = require("../mobx4")
const { observable, _getAdministration, reaction, makeObservable } = mobx
const iterall = require("iterall")
+let consoleWarnMock
+afterEach(() => {
+ consoleWarnMock?.mockRestore()
+})
+
+expect.addEqualityTesters([
+ function (a, b, ...rest) {
+ if (a instanceof LegacyObservableArray || b instanceof LegacyObservableArray) {
+ return this.equals([...a], [...b], ...rest)
+ }
+ return undefined
+ }
+])
+
test("test1", function () {
const a = observable.array([])
expect(a.length).toBe(0)
@@ -139,6 +154,34 @@ test("find(findIndex) and remove", function () {
expect(a.remove(20)).toBe(false)
})
+test("findLast(findLastIndex) and remove", function () {
+ const a = mobx.observable([10, 20, 20])
+ let idx = -1
+ function predicate(item, index) {
+ if (item === 20) {
+ idx = index
+ return true
+ }
+ return false
+ }
+ ;[].findLastIndex;
+ expect(a.findLast(predicate)).toBe(20)
+ expect(a.findLastIndex(predicate)).toBe(2)
+ expect(a.findLast(predicate)).toBe(20)
+
+ expect(a.remove(20)).toBe(true)
+ expect(a.find(predicate)).toBe(20)
+ expect(idx).toBe(1)
+ expect(a.findIndex(predicate)).toBe(1)
+ idx = -1
+ expect(a.remove(20)).toBe(true)
+ expect(a.findLast(predicate)).toBe(undefined)
+ expect(idx).toBe(-1)
+ expect(a.findLastIndex(predicate)).toBe(-1)
+
+ expect(a.remove(20)).toBe(false)
+})
+
test("concat should automatically slice observable arrays, #260", () => {
const a1 = mobx.observable([1, 2])
const a2 = mobx.observable([3, 4])
@@ -572,6 +615,8 @@ test("correct array should be passed to callbacks #2326", () => {
"filter",
"find",
"findIndex",
+ "findLast",
+ "findLastIndex",
"flatMap",
"forEach",
"map",
@@ -592,7 +637,9 @@ test("very long arrays can be safely passed to nativeArray.concat #2379", () =>
expect(longObservableArray).toEqual(longNativeArray)
expect(longObservableArray[9000]).toBe(longNativeArray[9000])
expect(longObservableArray[9999]).toBe(longNativeArray[9999])
+ consoleWarnMock = jest.spyOn(console, "warn").mockImplementation(() => {})
expect(longObservableArray[10000]).toBe(longNativeArray[10000])
+ expect(consoleWarnMock).toMatchSnapshot()
const expectedArray = nativeArray.concat(longNativeArray)
const actualArray = nativeArray.concat(longObservableArray.slice()) // NOTE: in MobX4 slice is needed
@@ -749,6 +796,31 @@ describe("dehances", () => {
expect([...array.values()]).toEqual([...dehanced.values()])
})
+ test("toReversed", () => {
+ expect(array.toReversed()).toEqual(dehanced.toReversed())
+ })
+
+ test("toSorted", () => {
+ expect(array.toSorted()).toEqual(dehanced.toSorted())
+ })
+
+ test("toSorted with args", () => {
+ expect(array.toSorted((a, b) => a - b)).toEqual(dehanced.toSorted((a, b) => a - b))
+ })
+
+ test("toSpliced", () => {
+ expect(array.toSpliced(1, 2)).toEqual(dehanced.toSpliced(1, 2))
+ })
+
+ test("with", () => {
+ expect(array.with(1, 5)).toEqual(dehanced.with(1, 5))
+ })
+
+ test("at", () => {
+ expect(array.at(1)).toEqual(dehanced.at(1))
+ expect(array.at(-1)).toEqual(dehanced.at(-1))
+ })
+
test("flat/flatMap", () => {
// not supported in V4
})
diff --git a/packages/mobx/__tests__/v4/base/autorun.js b/packages/mobx/__tests__/v4/base/autorun.js
index a238db16f4..ed809ad2c8 100644
--- a/packages/mobx/__tests__/v4/base/autorun.js
+++ b/packages/mobx/__tests__/v4/base/autorun.js
@@ -40,7 +40,7 @@ test("autorun can be disposed on first run", function () {
test("autorun warns when passed an action", function () {
const action = mobx.action(() => {})
expect.assertions(1)
- expect(() => mobx.autorun(action)).toThrowError(/Autorun does not accept actions/)
+ expect(() => mobx.autorun(action)).toThrow(/Autorun does not accept actions/)
})
test("autorun batches automatically", function () {
diff --git a/packages/mobx/__tests__/v4/base/extras.js b/packages/mobx/__tests__/v4/base/extras.js
index 8fd4927b9f..51306bea85 100644
--- a/packages/mobx/__tests__/v4/base/extras.js
+++ b/packages/mobx/__tests__/v4/base/extras.js
@@ -139,25 +139,25 @@ test("get atom", function () {
expect(atom(a)).toBe(ovClassName)
expect(atom(b, "a")).toBe(ovClassName)
- expect(() => atom(b)).toThrowError(/please specify a property/)
- expect(() => atom(b, "b")).toThrowError(
+ expect(() => atom(b)).toThrow(/please specify a property/)
+ expect(() => atom(b, "b")).toThrow(
/no observable property 'b' found on the observable object 'ObservableObject@2'/
)
expect(atom(c)).toBe(atomClassName) // returns ke, "bla".constructor, === "Atomys
expect(atom(c, "a")).toBe(ovClassName) // returns ent, "bla".constructor, === "Atomry
expect(atom(c, "b")).toBe(ovClassName) // returns has entry (see autoru, "bla", "Atomn)
- expect(() => atom(c, "c")).toThrowError(
+ expect(() => atom(c, "c")).toThrow(
/the entry 'c' does not exist in the observable map 'ObservableMap@3'/
)
expect(atom(d)).toBe(atomClassName)
- expect(() => atom(d, 0)).toThrowError(/It is not possible to get index atoms from arrays/)
+ expect(() => atom(d, 0)).toThrow(/It is not possible to get index atoms from arrays/)
expect(atom(e)).toBe(mobx.computed(() => {}).constructor.name)
expect(atom(f)).toBe(mobx.Reaction.name)
- expect(() => atom(g)).toThrowError(/please specify a property/)
+ expect(() => atom(g)).toThrow(/please specify a property/)
expect(atom(g, "a")).toBe(ovClassName)
f()
@@ -188,19 +188,19 @@ test("get debug name", function () {
expect(name(a)).toBe("ObservableValue@1")
expect(name(b, "a")).toBe("ObservableObject@2.a")
- expect(() => name(b, "b")).toThrowError(
+ expect(() => name(b, "b")).toThrow(
/no observable property 'b' found on the observable object 'ObservableObject@2'/
)
expect(name(c)).toBe("ObservableMap@3") // returns ke, "bla"ys
expect(name(c, "a")).toBe("ObservableMap@3.a") // returns ent, "bla"ry
expect(name(c, "b")).toBe("ObservableMap@3.b?") // returns has entry (see autoru, "bla"n)
- expect(() => name(c, "c")).toThrowError(
+ expect(() => name(c, "c")).toThrow(
/the entry 'c' does not exist in the observable map 'ObservableMap@3'/
)
expect(name(d)).toBe("ObservableArray@4")
- expect(() => name(d, 0)).toThrowError(/It is not possible to get index atoms from arrays/)
+ expect(() => name(d, 0)).toThrow(/It is not possible to get index atoms from arrays/)
expect(name(e)).toBe("ComputedValue@5")
expect(name(f)).toBe("Autorun@6")
@@ -240,19 +240,19 @@ test("get administration", function () {
expect(adm(b, "a")).toBe(ovClassName)
expect(adm(b)).toBe(b[$mobx].constructor.name)
- expect(() => adm(b, "b")).toThrowError(
+ expect(() => adm(b, "b")).toThrow(
/no observable property 'b' found on the observable object 'ObservableObject@2'/
)
expect(adm(c)).toBe(mapClassName)
expect(adm(c, "a")).toBe(ovClassName)
expect(adm(c, "b")).toBe(ovClassName)
- expect(() => adm(c, "c")).toThrowError(
+ expect(() => adm(c, "c")).toThrow(
/the entry 'c' does not exist in the observable map 'ObservableMap@3'/
)
expect(adm(d)).toBe(d[$mobx].constructor.name)
- expect(() => adm(d, 0)).toThrowError(/It is not possible to get index atoms from arrays/)
+ expect(() => adm(d, 0)).toThrow(/It is not possible to get index atoms from arrays/)
expect(adm(e)).toBe(mobx.computed(() => {}).constructor.name)
expect(adm(f)).toBe(mobx.Reaction.name)
diff --git a/packages/mobx/__tests__/v4/base/makereactive.js b/packages/mobx/__tests__/v4/base/makereactive.js
index 2ea3dc12fe..c7ebbb3d9d 100644
--- a/packages/mobx/__tests__/v4/base/makereactive.js
+++ b/packages/mobx/__tests__/v4/base/makereactive.js
@@ -450,7 +450,7 @@ test("as structure view", function () {
})
test("540 - extendobservable should not report cycles", function () {
- expect(() => m.extendObservable(Object.freeze({}), {})).toThrowError(
+ expect(() => m.extendObservable(Object.freeze({}), {})).toThrow(
/Cannot make the designated object observable/
)
@@ -463,7 +463,7 @@ test("540 - extendobservable should not report cycles", function () {
}
objWrapper.value = obj
- expect(() => mobx.extendObservable(objWrapper, objWrapper.value)).toThrowError(
+ expect(() => mobx.extendObservable(objWrapper, objWrapper.value)).toThrow(
/Extending an object with another observable \(object\) is not supported/
)
})
diff --git a/packages/mobx/__tests__/v4/base/observables.js b/packages/mobx/__tests__/v4/base/observables.js
index 1bd486d602..a67f07e14d 100644
--- a/packages/mobx/__tests__/v4/base/observables.js
+++ b/packages/mobx/__tests__/v4/base/observables.js
@@ -1156,7 +1156,7 @@ test("forcefully tracked reaction should still yield valid results", function ()
transaction(function () {
x.set(4)
a.track(identity)
- expect(a.isScheduled()).toBe(true)
+ expect(a.isScheduled).toBe(true)
expect(z).toBe(4)
expect(runCount).toBe(2)
})
@@ -1166,17 +1166,17 @@ test("forcefully tracked reaction should still yield valid results", function ()
transaction(function () {
x.set(5)
- expect(a.isScheduled()).toBe(true)
+ expect(a.isScheduled).toBe(true)
a.track(identity)
expect(z).toBe(5)
expect(runCount).toBe(3)
- expect(a.isScheduled()).toBe(true)
+ expect(a.isScheduled).toBe(true)
x.set(6)
expect(z).toBe(5)
expect(runCount).toBe(3)
})
- expect(a.isScheduled()).toBe(false)
+ expect(a.isScheduled).toBe(false)
expect(z).toBe(6)
expect(runCount).toBe(4)
})
@@ -1430,7 +1430,7 @@ test("support computed property getters / setters", () => {
a.size = 3
expect(a.volume).toBe(9)
- expect(() => (a.volume = 9)).toThrowError(
+ expect(() => (a.volume = 9)).toThrow(
/It is not possible to assign a new value to a computed value/
)
@@ -1493,7 +1493,7 @@ test("helpful error for self referencing setter", function () {
}
})
- expect(() => (a.y = 2)).toThrowError(/The setter of computed value/)
+ expect(() => (a.y = 2)).toThrow(/The setter of computed value/)
})
test("#558 boxed observables stay boxed observables", function () {
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/action.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/action.js.snap
index 331b296b70..6d76a6d527 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/action.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/action.js.snap
@@ -1,14 +1,14 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`error logging, #1836 - 1 1`] = `
-Array [
+[
" [mobx] (error in reaction 'Autorun@44' suppressed, fix error of causing action below)",
" Error: Action error",
]
`;
exports[`error logging, #1836 - 2 1`] = `
-Array [
+[
" [mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[Autorun@46]'",
]
`;
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/extras.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/extras.js.snap
index 420500fb07..a40f7d50de 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/extras.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/extras.js.snap
@@ -1,8 +1,8 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`spy 1 1`] = `
-Array [
- Object {
+[
+ {
"debugObjectName": "ObservableValue@5",
"newValue": 4,
"observableKind": "value",
@@ -10,23 +10,23 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"debugObjectName": "ComputedValue@6",
"newValue": 8,
"observableKind": "computed",
"oldValue": 6,
"type": "update",
},
- Object {
+ {
"name": "Autorun@7",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/flow.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/flow.js.snap
index 781d2f96ca..a3c1c0602d 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/flow.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/flow.js.snap
@@ -1,28 +1,28 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`it should support logging 1`] = `
-Array [
- Object {
- "arguments": Array [
+[
+ {
+ "arguments": [
2,
],
"name": "myaction - runid: 6 - init",
"spyReportStart": true,
"type": "action",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "arguments": Array [
+ {
+ "arguments": [
undefined,
],
"name": "myaction - runid: 6 - yield 0",
"spyReportStart": true,
"type": "action",
},
- Object {
+ {
"debugObjectName": "ObservableObject@7",
"name": "a",
"newValue": 2,
@@ -31,23 +31,23 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "arguments": Array [
+ {
+ "arguments": [
5,
],
"name": "myaction - runid: 6 - yield 1",
"spyReportStart": true,
"type": "action",
},
- Object {
+ {
"debugObjectName": "ObservableObject@7",
"name": "a",
"newValue": 5,
@@ -56,11 +56,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@7",
"name": "a",
"newValue": 4,
@@ -69,23 +69,23 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "arguments": Array [
+ {
+ "arguments": [
3,
],
"name": "myaction - runid: 6 - yield 2",
"spyReportStart": true,
"type": "action",
},
- Object {
+ {
"debugObjectName": "ObservableObject@7",
"name": "a",
"newValue": 3,
@@ -94,11 +94,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/makereactive.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/makereactive.js.snap
index bc78201b69..8cb8eface6 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/makereactive.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/makereactive.js.snap
@@ -1,3 +1,3 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`computed value 1`] = `"ComputedValue@1[() => 3]"`;
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/object-api.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/object-api.js.snap
index 70e287ca16..1f032d429e 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/object-api.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/object-api.js.snap
@@ -1,19 +1,19 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`observe & intercept 1`] = `
-Array [
- Object {
- "intercept": Object {
+[
+ {
+ "intercept": {
"name": "b",
- "newValue": Object {
+ "newValue": {
"title": "get tea",
},
"object": "skip",
"type": "add",
},
},
- Object {
- "intercept": Object {
+ {
+ "intercept": {
"name": "a",
"object": "skip",
"type": "remove",
@@ -23,12 +23,12 @@ Array [
`;
exports[`observe & intercept 2`] = `
-Array [
- Object {
- "observe": Object {
+[
+ {
+ "observe": {
"debugObjectName": "TestObject",
"name": "b",
- "newValue": Object {
+ "newValue": {
"title": "get tea",
},
"object": "skip",
@@ -36,13 +36,13 @@ Array [
"type": "add",
},
},
- Object {
- "observe": Object {
+ {
+ "observe": {
"debugObjectName": "TestObject",
"name": "a",
"object": "skip",
"observableKind": "object",
- "oldValue": Object {
+ "oldValue": {
"title": "get coffee",
},
"type": "remove",
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/observables.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/observables.js.snap
index 9d3551dee5..1fc3d3ed56 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/observables.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/observables.js.snap
@@ -1,8 +1,8 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`delay autorun until end of transaction 1`] = `
-Array [
- Object {
+[
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 3,
@@ -11,11 +11,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 4,
@@ -24,12 +24,12 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"end1",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 5,
@@ -38,19 +38,19 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"end2",
- Object {
+ {
"name": "test",
"spyReportStart": true,
"type": "reaction",
},
"auto",
"calc y",
- Object {
+ {
"debugObjectName": "ObservableObject@1.b",
"newValue": 5,
"observableKind": "computed",
@@ -59,12 +59,12 @@ Array [
},
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"post trans1",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 6,
@@ -74,29 +74,29 @@ Array [
"type": "update",
},
"calc y",
- Object {
+ {
"debugObjectName": "ObservableObject@1.b",
"newValue": 6,
"observableKind": "computed",
"oldValue": 5,
"type": "update",
},
- Object {
+ {
"name": "test",
"spyReportStart": true,
"type": "reaction",
},
"auto",
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"post trans2",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": 3,
@@ -105,7 +105,7 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
@@ -114,11 +114,11 @@ Array [
`;
exports[`issue 50 1`] = `
-Array [
+[
"auto",
"calc c",
"transstart",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "a",
"newValue": false,
@@ -127,11 +127,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "b",
"newValue": true,
@@ -140,26 +140,26 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"transpreend",
- Object {
+ {
"name": "ar",
"spyReportStart": true,
"type": "reaction",
},
"auto",
"calc c",
- Object {
+ {
"debugObjectName": "ObservableObject@1.c",
"newValue": true,
"observableKind": "computed",
"oldValue": false,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
@@ -168,11 +168,11 @@ Array [
`;
exports[`verify transaction events 1`] = `
-Array [
+[
"auto",
"calc c",
"transstart",
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "b",
"newValue": 2,
@@ -181,26 +181,26 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
"transpreend",
"calc c",
- Object {
+ {
"debugObjectName": "ObservableObject@1.c",
"newValue": 2,
"observableKind": "computed",
"oldValue": 1,
"type": "update",
},
- Object {
+ {
"name": "ar",
"spyReportStart": true,
"type": "reaction",
},
"auto",
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/proxies.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/proxies.js.snap
index c6fc355453..d6a79f19f5 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/proxies.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/proxies.js.snap
@@ -1,32 +1,32 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`decorate proxies 1`] = `
-Object {
- "b": Object {
+{
+ "b": {
"configurable": true,
"enumerable": true,
"get": [Function],
"set": [Function],
},
- "double": Object {
+ "double": {
"configurable": true,
"enumerable": false,
"value": [Function],
"writable": false,
},
- "x": Object {
+ "x": {
"configurable": true,
"enumerable": true,
"get": [Function],
"set": [Function],
},
- "y": Object {
+ "y": {
"configurable": true,
"enumerable": false,
"get": [Function],
"set": [Function],
},
- Symbol(mobx administration): Object {
+ Symbol(mobx administration): {
"configurable": true,
"enumerable": false,
"value": "(omitted)",
@@ -36,32 +36,32 @@ Object {
`;
exports[`extend proxies 1`] = `
-Object {
- "b": Object {
+{
+ "b": {
"configurable": true,
"enumerable": true,
"get": [Function],
"set": [Function],
},
- "double": Object {
+ "double": {
"configurable": true,
"enumerable": false,
"value": [Function],
"writable": false,
},
- "x": Object {
+ "x": {
"configurable": true,
"enumerable": true,
"get": [Function],
"set": [Function],
},
- "y": Object {
+ "y": {
"configurable": true,
"enumerable": false,
"get": [Function],
"set": [Function],
},
- Symbol(mobx administration): Object {
+ Symbol(mobx administration): {
"configurable": true,
"enumerable": false,
"value": "(omitted)",
@@ -71,32 +71,32 @@ Object {
`;
exports[`non-proxied object 1`] = `
-Object {
- "b": Object {
+{
+ "b": {
"configurable": true,
"enumerable": true,
"value": 4,
"writable": true,
},
- "double": Object {
+ "double": {
"configurable": true,
"enumerable": false,
"value": [Function],
"writable": false,
},
- "x": Object {
+ "x": {
"configurable": true,
"enumerable": true,
"get": [Function],
"set": [Function],
},
- "y": Object {
+ "y": {
"configurable": true,
"enumerable": false,
"get": [Function],
"set": [Function],
},
- Symbol(mobx administration): Object {
+ Symbol(mobx administration): {
"configurable": true,
"enumerable": false,
"value": "(omitted)",
diff --git a/packages/mobx/__tests__/v5/base/__snapshots__/spy.js.snap b/packages/mobx/__tests__/v5/base/__snapshots__/spy.js.snap
index fb2ea0b73a..646f594bf8 100644
--- a/packages/mobx/__tests__/v5/base/__snapshots__/spy.js.snap
+++ b/packages/mobx/__tests__/v5/base/__snapshots__/spy.js.snap
@@ -1,13 +1,13 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`spy error 1`] = `
-Array [
- Object {
+[
+ {
"name": "autorun",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1.y",
"newValue": 4,
"observableKind": "computed",
@@ -16,11 +16,11 @@ Array [
},
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "x",
"newValue": 3,
@@ -29,7 +29,7 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1.y",
"newValue": CaughtException {
"cause": "Oops",
@@ -38,34 +38,34 @@ Array [
"oldValue": 4,
"type": "update",
},
- Object {
+ {
"name": "autorun",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"error": "Oops",
"message": "[mobx] Encountered an uncaught exception that was thrown by a reaction or observer component, in: 'Reaction[autorun]'",
"name": "autorun",
"type": "error",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "arguments": Array [
+ {
+ "arguments": [
4,
],
"name": "setX",
"spyReportStart": true,
"type": "action",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1",
"name": "x",
"newValue": 4,
@@ -74,11 +74,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@1.y",
"newValue": 8,
"observableKind": "computed",
@@ -87,16 +87,16 @@ Array [
},
"type": "update",
},
- Object {
+ {
"name": "autorun",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
@@ -104,14 +104,14 @@ Array [
`;
exports[`spy output 1`] = `
-Array [
- Object {
+[
+ {
"debugObjectName": "ObservableValue@1",
"newValue": "2",
"observableKind": "value",
"type": "create",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 3,
"observableKind": "value",
@@ -119,11 +119,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "c",
"newValue": 4,
@@ -131,11 +131,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "c",
"newValue": 5,
@@ -144,11 +144,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "d",
"newValue": 6,
@@ -156,11 +156,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableObject@2",
"name": "d",
"newValue": 7,
@@ -169,12 +169,12 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "added": Array [
+ {
+ "added": [
1,
2,
],
@@ -182,17 +182,17 @@ Array [
"debugObjectName": "ObservableArray@3",
"index": 0,
"observableKind": "array",
- "removed": Array [],
+ "removed": [],
"removedCount": 0,
"spyReportStart": true,
"type": "splice",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "added": Array [
+ {
+ "added": [
3,
4,
],
@@ -200,33 +200,33 @@ Array [
"debugObjectName": "ObservableArray@3",
"index": 2,
"observableKind": "array",
- "removed": Array [],
+ "removed": [],
"removedCount": 0,
"spyReportStart": true,
"type": "splice",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "added": Array [],
+ {
+ "added": [],
"addedCount": 0,
"debugObjectName": "ObservableArray@3",
"index": 0,
"observableKind": "array",
- "removed": Array [
+ "removed": [
1,
],
"removedCount": 1,
"spyReportStart": true,
"type": "splice",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableArray@3",
"index": 2,
"newValue": 5,
@@ -235,11 +235,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "g",
"newValue": 1,
@@ -247,11 +247,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "g",
"observableKind": "map",
@@ -259,11 +259,11 @@ Array [
"spyReportStart": true,
"type": "delete",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "i",
"newValue": 5,
@@ -271,11 +271,11 @@ Array [
"spyReportStart": true,
"type": "add",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableMap@4",
"name": "i",
"newValue": 6,
@@ -284,16 +284,16 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 6,
"observableKind": "computed",
@@ -302,11 +302,11 @@ Array [
},
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 4,
"observableKind": "value",
@@ -314,27 +314,27 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 8,
"observableKind": "computed",
"oldValue": 6,
"type": "update",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 5,
"observableKind": "value",
@@ -342,11 +342,11 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 6,
"observableKind": "value",
@@ -354,35 +354,35 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 12,
"observableKind": "computed",
"oldValue": 8,
"type": "update",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
- "arguments": Array [
+ {
+ "arguments": [
7,
],
"name": "myTestAction",
"spyReportStart": true,
"type": "action",
},
- Object {
+ {
"debugObjectName": "ObservableValue@1",
"newValue": 7,
"observableKind": "value",
@@ -390,27 +390,27 @@ Array [
"spyReportStart": true,
"type": "update",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"debugObjectName": "ComputedValue@5",
"newValue": 14,
"observableKind": "computed",
"oldValue": 12,
"type": "update",
},
- Object {
+ {
"name": "Autorun@6",
"spyReportStart": true,
"type": "reaction",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
- Object {
+ {
"spyReportEnd": true,
"type": "report-end",
},
diff --git a/packages/mobx/__tests__/v5/base/action.js b/packages/mobx/__tests__/v5/base/action.js
index cfe782d116..5702578252 100644
--- a/packages/mobx/__tests__/v5/base/action.js
+++ b/packages/mobx/__tests__/v5/base/action.js
@@ -646,3 +646,18 @@ test("auto action should not update state from inside a derivation", async () =>
})
d()
})
+
+test("action forwards toString of underlying function", async () => {
+ const fn = () => {
+ /* not actually doing anything */
+ }
+ fn.a = 42
+ fn.toString = function () {
+ return `toString referencing this, a=${this.a}`
+ }
+
+ const act = mobx.action(fn)
+
+ expect(fn.toString()).toBe("toString referencing this, a=42")
+ expect(act.toString()).toBe("toString referencing this, a=42")
+})
diff --git a/packages/mobx/__tests__/v5/base/array.js b/packages/mobx/__tests__/v5/base/array.js
index 3252f8ece9..5b07513987 100644
--- a/packages/mobx/__tests__/v5/base/array.js
+++ b/packages/mobx/__tests__/v5/base/array.js
@@ -4,6 +4,11 @@ const mobx = require("../../../src/mobx.ts")
const { observable, when, _getAdministration, reaction, computed, makeObservable, autorun } = mobx
const iterall = require("iterall")
+let consoleWarnSpy
+afterEach(() => {
+ consoleWarnSpy?.mockRestore()
+})
+
test("test1", function () {
const a = observable.array([])
expect(a.length).toBe(0)
@@ -173,6 +178,34 @@ test("find(findIndex) and remove", function () {
expect(a.remove(20)).toBe(false)
})
+test("findLast(findLastIndex) and remove", function () {
+ const a = mobx.observable([10, 20, 20])
+ let idx = -1
+ function predicate(item, index) {
+ if (item === 20) {
+ idx = index
+ return true
+ }
+ return false
+ }
+ ;[].findLastIndex;
+ expect(a.findLast(predicate)).toBe(20)
+ expect(a.findLastIndex(predicate)).toBe(2)
+ expect(a.findLast(predicate)).toBe(20)
+
+ expect(a.remove(20)).toBe(true)
+ expect(a.find(predicate)).toBe(20)
+ expect(idx).toBe(1)
+ expect(a.findIndex(predicate)).toBe(1)
+ idx = -1
+ expect(a.remove(20)).toBe(true)
+ expect(a.findLast(predicate)).toBe(undefined)
+ expect(idx).toBe(-1)
+ expect(a.findLastIndex(predicate)).toBe(-1)
+
+ expect(a.remove(20)).toBe(false)
+})
+
test("concat should automatically slice observable arrays, #260", () => {
const a1 = mobx.observable([1, 2])
const a2 = mobx.observable([3, 4])
@@ -397,26 +430,6 @@ test("array exposes correct keys", () => {
expect(keys).toEqual(["0", "1"])
})
-test("accessing out of bound values throws", () => {
- const a = mobx.observable([])
-
- let warns = 0
- const baseWarn = console.warn
- console.warn = () => {
- warns++
- }
-
- a[0] // out of bounds
- a[1] // out of bounds
-
- expect(warns).toBe(2)
-
- expect(() => (a[0] = 3)).not.toThrow()
- expect(() => (a[2] = 4)).toThrow(/Index out of bounds, 2 is larger than 1/)
-
- console.warn = baseWarn
-})
-
test("replace can handle large arrays", () => {
const a = mobx.observable([])
const b = []
@@ -645,6 +658,8 @@ test("correct array should be passed to callbacks #2326", () => {
"filter",
"find",
"findIndex",
+ "findLast",
+ "findLastIndex",
"flatMap",
"forEach",
"map",
@@ -822,6 +837,31 @@ describe("dehances", () => {
expect([...array.values()]).toEqual([...dehanced.values()])
})
+ test("toReversed", () => {
+ expect(array.toReversed()).toEqual(dehanced.toReversed())
+ })
+
+ test("toSorted", () => {
+ expect(array.toSorted()).toEqual(dehanced.toSorted())
+ })
+
+ test("toSorted with args", () => {
+ expect(array.toSorted((a, b) => a - b)).toEqual(dehanced.toSorted((a, b) => a - b))
+ })
+
+ test("toSpliced", () => {
+ expect(array.toSpliced(1, 2)).toEqual(dehanced.toSpliced(1, 2))
+ })
+
+ test("with", () => {
+ expect(array.with(1, 5)).toEqual(dehanced.with(1, 5))
+ })
+
+ test("at", () => {
+ expect(array.at(1)).toEqual(dehanced.at(1))
+ expect(array.at(-1)).toEqual(dehanced.at(-1))
+ })
+
test("flat/flatMap", () => {
const nestedArray = [{ value: 1 }, [{ value: 2 }, [{ value: 3 }]]]
const dehancedNestedArray = nestedArray.map(dehancer)
@@ -860,3 +900,17 @@ test("reduce without initial value #2432", () => {
expect(observableArraySum).toEqual(arraySum)
expect(arrayReducerArgs).toEqual(observableArrayReducerArgs)
})
+
+test("accessing out of bound indices is supported", () => {
+ consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {
+ throw new Error(`Unexpected console.warn call`)
+ })
+
+ const array = observable([])
+
+ array[1]
+ array[2]
+ array[1001] = "foo"
+ expect(array.length).toBe(1002)
+ expect(array[1001]).toBe("foo")
+})
diff --git a/packages/mobx/__tests__/v5/base/autorun.js b/packages/mobx/__tests__/v5/base/autorun.js
index c84a9b3c54..72499f0dc3 100644
--- a/packages/mobx/__tests__/v5/base/autorun.js
+++ b/packages/mobx/__tests__/v5/base/autorun.js
@@ -37,10 +37,47 @@ test("autorun can be disposed on first run", function () {
expect(values).toEqual([1])
})
+test("autorun can be disposed using AbortSignal", function () {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ mobx.autorun(
+ () => {
+ values.push(a.get())
+ },
+ { signal: ac.signal }
+ )
+
+ a.set(2)
+ a.set(3)
+ ac.abort()
+ a.set(4)
+
+ expect(values).toEqual([1, 2, 3])
+})
+
+test("autorun should not run first time when passing already aborted AbortSignal", function () {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ ac.abort()
+
+ mobx.autorun(
+ () => {
+ values.push(a.get())
+ },
+ { signal: ac.signal }
+ )
+
+ expect(values).toEqual([])
+})
+
test("autorun warns when passed an action", function () {
const action = mobx.action(() => {})
expect.assertions(1)
- expect(() => mobx.autorun(action)).toThrowError(/Autorun does not accept actions/)
+ expect(() => mobx.autorun(action)).toThrow(/Autorun does not accept actions/)
})
test("autorun batches automatically", function () {
diff --git a/packages/mobx/__tests__/v5/base/autorunAsync.js b/packages/mobx/__tests__/v5/base/autorunAsync.js
index e3abc93da9..028a81c3a6 100644
--- a/packages/mobx/__tests__/v5/base/autorunAsync.js
+++ b/packages/mobx/__tests__/v5/base/autorunAsync.js
@@ -227,7 +227,7 @@ test("reaction accepts a scheduling function", function (done) {
test("autorunAsync warns when passed an action", function () {
const action = mobx.action(() => {})
expect.assertions(1)
- expect(() => mobx.autorun(action)).toThrowError(/Autorun does not accept actions/)
+ expect(() => mobx.autorun(action)).toThrow(/Autorun does not accept actions/)
})
test("whenWithTimeout should operate normally", done => {
diff --git a/packages/mobx/__tests__/v5/base/become-observed.ts b/packages/mobx/__tests__/v5/base/become-observed.ts
index d643ab63d7..a075422f4b 100644
--- a/packages/mobx/__tests__/v5/base/become-observed.ts
+++ b/packages/mobx/__tests__/v5/base/become-observed.ts
@@ -18,7 +18,7 @@ describe("become-observed", () => {
const cb = jest.fn()
onBecomeObserved(oMap, key, cb)
autorun(() => oMap.get(key))
- expect(cb).toBeCalled()
+ expect(cb).toHaveBeenCalled()
})
})
@@ -509,3 +509,20 @@ test("#2667", () => {
"onBecomeUnobservednew"
])
})
+
+test("works with ObservableSet #3595", () => {
+ const onSetObserved = jest.fn()
+ const onSetUnobserved = jest.fn()
+
+ const set = observable.set()
+
+ const disposeOBO = onBecomeObserved(set, onSetObserved)
+ const disposeOBU = onBecomeUnobserved(set, onSetUnobserved)
+ const diposeAutorun = autorun(() => set.size)
+ diposeAutorun()
+ disposeOBO()
+ disposeOBU()
+
+ expect(onSetObserved).toHaveBeenCalledTimes(1)
+ expect(onSetUnobserved).toHaveBeenCalledTimes(1)
+})
diff --git a/packages/mobx/__tests__/v5/base/errorhandling.js b/packages/mobx/__tests__/v5/base/errorhandling.js
index 566e37d373..533c674083 100644
--- a/packages/mobx/__tests__/v5/base/errorhandling.js
+++ b/packages/mobx/__tests__/v5/base/errorhandling.js
@@ -4,7 +4,7 @@ const utils = require("../../v5/utils/test-utils")
const { observable, computed, $mobx, autorun } = mobx
-const voidObserver = function () { }
+const voidObserver = function () {}
function checkGlobalState() {
const gs = mobx._getGlobalState()
@@ -35,7 +35,7 @@ test("exceptions in computed values can be recovered from", () => {
expect(a.y).toBe(2)
a.x = 2
- expect(() => a.y).toThrowError(/Uhoh/)
+ expect(() => a.y).toThrow(/Uhoh/)
checkGlobalState()
@@ -91,7 +91,7 @@ test("exception in autorun can be recovered from", () => {
// exception is also rethrown to each consumer
expect(() => {
expect(a.y).toBe(2) // old cached value!
- }).toThrowError(/Uhoh/)
+ }).toThrow(/Uhoh/)
expect(mobx.getAtom(a, "y").observers_.size).toBe(1)
expect(b).toBe(2)
@@ -148,7 +148,7 @@ test("deny state changes in views", function () {
m.reaction(
() => z.get(),
- () => { }
+ () => {}
)
expect(
utils.grabConsole(() => {
@@ -194,7 +194,7 @@ test("deny array change in view", function (done) {
}).not.toThrow()
m.reaction(
() => z.length,
- () => { }
+ () => {}
)
expect(
@@ -481,11 +481,11 @@ test("peeking inside erroring computed value doesn't bork (global) state", () =>
expect(() => {
b.get()
- }).toThrowError(/chocolademelk/)
+ }).toThrow(/chocolademelk/)
- expect(a.isPendingUnobservation_).toBe(false)
+ expect(a.isPendingUnobservation).toBe(false)
expect(a.observers_.size).toBe(0)
- expect(a.diffValue_).toBe(0)
+ expect(a.diffValue).toBe(0)
expect(a.lowestObserverState_).toBe(-1)
expect(a.hasUnreportedChange_).toBe(false)
expect(a.value_).toBe(1)
@@ -493,15 +493,15 @@ test("peeking inside erroring computed value doesn't bork (global) state", () =>
expect(b.dependenciesState_).toBe(-1) // NOT_TRACKING
expect(b.observing_.length).toBe(0)
expect(b.newObserving_).toBe(null)
- expect(b.isPendingUnobservation_).toBe(false)
+ expect(b.isPendingUnobservation).toBe(false)
expect(b.observers_.size).toBe(0)
- expect(b.diffValue_).toBe(0)
+ expect(b.diffValue).toBe(0)
expect(b.lowestObserverState_).toBe(0)
expect(b.unboundDepsCount_).toBe(0)
expect(() => {
b.get()
- }).toThrowError(/chocolademelk/)
- expect(b.isComputing_).toBe(false)
+ }).toThrow(/chocolademelk/)
+ expect(b.isComputing).toBe(false)
checkGlobalState()
})
@@ -521,9 +521,9 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
expect(r).toBe(1)
test("it should update correctly initially", () => {
- expect(a.isPendingUnobservation_).toBe(false)
+ expect(a.isPendingUnobservation).toBe(false)
expect(a.observers_.size).toBe(1)
- expect(a.diffValue_).toBe(0)
+ expect(a.diffValue).toBe(0)
expect(a.lowestObserverState_).toBe(-1)
expect(a.hasUnreportedChange_).toBe(false)
expect(a.value_).toBe(1)
@@ -531,23 +531,23 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
expect(b.dependenciesState_).toBe(0)
expect(b.observing_.length).toBe(1)
expect(b.newObserving_).toBe(null)
- expect(b.isPendingUnobservation_).toBe(false)
+ expect(b.isPendingUnobservation).toBe(false)
expect(b.observers_.size).toBe(1)
- expect(b.diffValue_).toBe(0)
+ expect(b.diffValue).toBe(0)
expect(b.lowestObserverState_).toBe(0)
expect(b.unboundDepsCount_).toBe(1) // value is always the last bound amount of observers
expect(b.value_).toBe(1)
- expect(b.isComputing_).toBe(false)
+ expect(b.isComputing).toBe(false)
expect(c.dependenciesState_).toBe(0)
expect(c.observing_.length).toBe(1)
expect(c.newObserving_).toBe(null)
- expect(c.diffValue_).toBe(0)
+ expect(c.diffValue).toBe(0)
expect(c.unboundDepsCount_).toBe(1)
- expect(c.isDisposed_).toBe(false)
- expect(c.isScheduled_).toBe(false)
- expect(c.isTrackPending_).toBe(false)
- expect(c.isRunning_).toBe(false)
+ expect(c.isDisposed).toBe(false)
+ expect(c.isScheduled).toBe(false)
+ expect(c.isTrackPending).toBe(false)
+ expect(c.isRunning).toBe(false)
checkGlobalState()
})
@@ -558,9 +558,9 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
}, /chocolademelk/)
expect(r).toBe(2)
- expect(a.isPendingUnobservation_).toBe(false)
+ expect(a.isPendingUnobservation).toBe(false)
expect(a.observers_.size).toBe(1)
- expect(a.diffValue_).toBe(0)
+ expect(a.diffValue).toBe(0)
expect(a.lowestObserverState_).toBe(0)
expect(a.hasUnreportedChange_).toBe(false)
expect(a.value_).toBe(2)
@@ -568,23 +568,23 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
expect(b.dependenciesState_).toBe(0) // up to date (for what it's worth)
expect(b.observing_.length).toBe(1)
expect(b.newObserving_).toBe(null)
- expect(b.isPendingUnobservation_).toBe(false)
+ expect(b.isPendingUnobservation).toBe(false)
expect(b.observers_.size).toBe(1)
- expect(b.diffValue_).toBe(0)
+ expect(b.diffValue).toBe(0)
expect(b.lowestObserverState_).toBe(0)
expect(b.unboundDepsCount_).toBe(1)
- expect(b.isComputing_).toBe(false)
- expect(() => b.get()).toThrowError(/chocolademelk/)
+ expect(b.isComputing).toBe(false)
+ expect(() => b.get()).toThrow(/chocolademelk/)
expect(c.dependenciesState_).toBe(0)
expect(c.observing_.length).toBe(1)
expect(c.newObserving_).toBe(null)
- expect(c.diffValue_).toBe(0)
+ expect(c.diffValue).toBe(0)
expect(c.unboundDepsCount_).toBe(1)
- expect(c.isDisposed_).toBe(false)
- expect(c.isScheduled_).toBe(false)
- expect(c.isTrackPending_).toBe(false)
- expect(c.isRunning_).toBe(false)
+ expect(c.isDisposed).toBe(false)
+ expect(c.isScheduled).toBe(false)
+ expect(c.isTrackPending).toBe(false)
+ expect(c.isRunning).toBe(false)
checkGlobalState()
})
@@ -594,9 +594,9 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
a.set(3)
expect(r).toBe(3)
- expect(a.isPendingUnobservation_).toBe(false)
+ expect(a.isPendingUnobservation).toBe(false)
expect(a.observers_.size).toBe(1)
- expect(a.diffValue_).toBe(0)
+ expect(a.diffValue).toBe(0)
expect(a.lowestObserverState_).toBe(0)
expect(a.hasUnreportedChange_).toBe(false)
expect(a.value_).toBe(3)
@@ -604,23 +604,23 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
expect(b.dependenciesState_).toBe(0) // up to date
expect(b.observing_.length).toBe(1)
expect(b.newObserving_).toBe(null)
- expect(b.isPendingUnobservation_).toBe(false)
+ expect(b.isPendingUnobservation).toBe(false)
expect(b.observers_.size).toBe(1)
- expect(b.diffValue_).toBe(0)
+ expect(b.diffValue).toBe(0)
expect(b.lowestObserverState_).toBe(0)
expect(b.unboundDepsCount_).toBe(1)
expect(b.value_).toBe(3)
- expect(b.isComputing_).toBe(false)
+ expect(b.isComputing).toBe(false)
expect(c.dependenciesState_).toBe(0)
expect(c.observing_.length).toBe(1)
expect(c.newObserving_).toBe(null)
- expect(c.diffValue_).toBe(0)
+ expect(c.diffValue).toBe(0)
expect(c.unboundDepsCount_).toBe(1)
- expect(c.isDisposed_).toBe(false)
- expect(c.isScheduled_).toBe(false)
- expect(c.isTrackPending_).toBe(false)
- expect(c.isRunning_).toBe(false)
+ expect(c.isDisposed).toBe(false)
+ expect(c.isScheduled).toBe(false)
+ expect(c.isTrackPending).toBe(false)
+ expect(c.isRunning).toBe(false)
checkGlobalState()
})
@@ -628,9 +628,9 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
test("it should clean up correctly", () => {
d()
- expect(a.isPendingUnobservation_).toBe(false)
+ expect(a.isPendingUnobservation).toBe(false)
expect(a.observers_.size).toBe(0)
- expect(a.diffValue_).toBe(0)
+ expect(a.diffValue).toBe(0)
expect(a.lowestObserverState_).toBe(0)
expect(a.hasUnreportedChange_).toBe(false)
expect(a.value_).toBe(3)
@@ -638,23 +638,23 @@ describe("peeking inside autorun doesn't bork (global) state", () => {
expect(b.dependenciesState_).toBe(-1) // not tracking
expect(b.observing_.length).toBe(0)
expect(b.newObserving_).toBe(null)
- expect(b.isPendingUnobservation_).toBe(false)
+ expect(b.isPendingUnobservation).toBe(false)
expect(b.observers_.size).toBe(0)
- expect(b.diffValue_).toBe(0)
+ expect(b.diffValue).toBe(0)
expect(b.lowestObserverState_).toBe(0)
expect(b.unboundDepsCount_).toBe(1)
expect(b.value_).not.toBe(3)
- expect(b.isComputing_).toBe(false)
+ expect(b.isComputing).toBe(false)
expect(c.dependenciesState_).toBe(-1)
expect(c.observing_.length).toBe(0)
expect(c.newObserving_).toBe(null)
- expect(c.diffValue_).toBe(0)
+ expect(c.diffValue).toBe(0)
expect(c.unboundDepsCount_).toBe(1)
- expect(c.isDisposed_).toBe(true)
- expect(c.isScheduled_).toBe(false)
- expect(c.isTrackPending_).toBe(false)
- expect(c.isRunning_).toBe(false)
+ expect(c.isDisposed).toBe(true)
+ expect(c.isScheduled).toBe(false)
+ expect(c.isTrackPending).toBe(false)
+ expect(c.isRunning).toBe(false)
expect(b.get()).toBe(3)
@@ -746,7 +746,7 @@ test("global error handling will be skipped when using disableErrorBoundaries -
mobx.autorun(function () {
throw "OOPS"
})
- }).toThrowError(/OOPS/)
+ }).toThrow(/OOPS/)
} finally {
mobx.configure({ disableErrorBoundaries: false })
mobx._resetGlobalState()
@@ -768,7 +768,7 @@ test("global error handling will be skipped when using disableErrorBoundaries -
)
expect(() => {
a.set(2)
- }).toThrowError(/OOPS/)
+ }).toThrow(/OOPS/)
d()
} finally {
@@ -827,7 +827,7 @@ describe("es5 compat warnings", () => {
expect(() => {
"z" in x
- }).not.toThrowError()
+ }).not.toThrow()
let e
autorun(() => {
@@ -845,7 +845,7 @@ describe("es5 compat warnings", () => {
expect(() => {
Object.getOwnPropertyNames(x)
- }).not.toThrowError()
+ }).not.toThrow()
autorun(() => {
try {
Object.getOwnPropertyNames(x)
@@ -866,4 +866,4 @@ describe("es5 compat warnings", () => {
})
})
-test("should throw when adding properties in ES5 compat mode", () => { })
+test("should throw when adding properties in ES5 compat mode", () => {})
diff --git a/packages/mobx/__tests__/v5/base/extendObservable.js b/packages/mobx/__tests__/v5/base/extendObservable.js
index b797821069..7fdfd4b635 100644
--- a/packages/mobx/__tests__/v5/base/extendObservable.js
+++ b/packages/mobx/__tests__/v5/base/extendObservable.js
@@ -7,7 +7,9 @@ import {
isObservableProp,
isComputedProp,
isAction,
- extendObservable
+ extendObservable,
+ observable,
+ reaction
} from "../../../src/mobx"
test("extendObservable should work", function () {
@@ -160,3 +162,15 @@ test("extendObservable should apply specified decorators", function () {
expect(isAction(box.someFunc)).toBe(true)
expect(box.someFunc()).toEqual(2)
})
+
+test("extendObservable notifies about added keys", () => {
+ let reactionCalled = false
+ const o = observable({})
+ const disposeReaction = reaction(
+ () => Object.keys(o),
+ () => (reactionCalled = true)
+ )
+ extendObservable(o, { x: 0 })
+ expect(reactionCalled).toBe(true)
+ disposeReaction()
+})
diff --git a/packages/mobx/__tests__/v5/base/extras.js b/packages/mobx/__tests__/v5/base/extras.js
index 11d09bf433..471b947abf 100644
--- a/packages/mobx/__tests__/v5/base/extras.js
+++ b/packages/mobx/__tests__/v5/base/extras.js
@@ -190,25 +190,25 @@ test("get atom", function () {
expect(atom(a)).toBe(ovClassName)
expect(atom(b, "a")).toBe(ovClassName)
- expect(() => atom(b)).toThrowError(/please specify a property/)
- expect(() => atom(b, "b")).toThrowError(
+ expect(() => atom(b)).toThrow(/please specify a property/)
+ expect(() => atom(b, "b")).toThrow(
/no observable property 'b' found on the observable object 'ObservableObject@2'/
)
expect(atom(c)).toBe(atomClassName) // returns ke, "bla".constructor, === "Atomys
expect(atom(c, "a")).toBe(ovClassName) // returns ent, "bla".constructor, === "Atomry
expect(atom(c, "b")).toBe(ovClassName) // returns has entry (see autoru, "bla", "Atomn)
- expect(() => atom(c, "c")).toThrowError(
+ expect(() => atom(c, "c")).toThrow(
/the entry 'c' does not exist in the observable map 'ObservableMap@3'/
)
expect(atom(d)).toBe(atomClassName)
- expect(() => atom(d, 0)).toThrowError(/It is not possible to get index atoms from arrays/)
+ expect(() => atom(d, 0)).toThrow(/It is not possible to get index atoms from arrays/)
expect(atom(e)).toBe(mobx.computed(() => {}).constructor.name_)
expect(atom(f)).toBe(mobx.Reaction.name_)
- expect(() => atom(g)).toThrowError(/please specify a property/)
+ expect(() => atom(g)).toThrow(/please specify a property/)
expect(atom(g, "a")).toBe(ovClassName)
f()
@@ -239,19 +239,19 @@ test("get debug name", function () {
expect(name(a)).toBe("ObservableValue@1")
expect(name(b, "a")).toBe("ObservableObject@2.a")
- expect(() => name(b, "b")).toThrowError(
+ expect(() => name(b, "b")).toThrow(
/no observable property 'b' found on the observable object 'ObservableObject@2'/
)
expect(name(c)).toBe("ObservableMap@3") // returns ke, "bla"ys
expect(name(c, "a")).toBe("ObservableMap@3.a") // returns ent, "bla"ry
expect(name(c, "b")).toBe("ObservableMap@3.b?") // returns has entry (see autoru, "bla"n)
- expect(() => name(c, "c")).toThrowError(
+ expect(() => name(c, "c")).toThrow(
/the entry 'c' does not exist in the observable map 'ObservableMap@3'/
)
expect(name(d)).toBe("ObservableArray@4")
- expect(() => name(d, 0)).toThrowError(/It is not possible to get index atoms from arrays/)
+ expect(() => name(d, 0)).toThrow(/It is not possible to get index atoms from arrays/)
expect(name(e)).toBe("ComputedValue@5")
expect(name(f)).toBe("Autorun@6")
@@ -293,24 +293,24 @@ test("get administration", function () {
expect(adm(b, "a")).toBe(ovClassName)
expect(adm(b)).toBe(b[$mobx].constructor.name_)
- expect(() => adm(b, "b")).toThrowError(
+ expect(() => adm(b, "b")).toThrow(
/no observable property 'b' found on the observable object 'ObservableObject@2'/
)
expect(adm(h, "a")).toBe(ovClassName)
expect(adm(h)).toBe(h[$mobx].constructor.name_)
- expect(() => adm(h, "b")).toThrowError(
+ expect(() => adm(h, "b")).toThrow(
/no observable property 'b' found on the observable object 'ObservableObject@8'/
)
expect(adm(c)).toBe(mapClassName)
expect(adm(c, "a")).toBe(ovClassName)
expect(adm(c, "b")).toBe(ovClassName)
- expect(() => adm(c, "c")).toThrowError(
+ expect(() => adm(c, "c")).toThrow(
/the entry 'c' does not exist in the observable map 'ObservableMap@3'/
)
expect(adm(d)).toBe(d[$mobx].constructor.name_)
- expect(() => adm(d, 0)).toThrowError(/It is not possible to get index atoms from arrays/)
+ expect(() => adm(d, 0)).toThrow(/It is not possible to get index atoms from arrays/)
expect(adm(e)).toBe(mobx.computed(() => {}).constructor.name_)
expect(adm(f)).toBe(mobx.Reaction.name_)
diff --git a/packages/mobx/__tests__/v5/base/makereactive.js b/packages/mobx/__tests__/v5/base/makereactive.js
index 6b39daabf5..2da8670bd9 100644
--- a/packages/mobx/__tests__/v5/base/makereactive.js
+++ b/packages/mobx/__tests__/v5/base/makereactive.js
@@ -457,7 +457,7 @@ test("as structure view", function () {
})
test("540 - extendobservable should not report cycles", function () {
- expect(() => m.extendObservable(Object.freeze({}), {})).toThrowError(
+ expect(() => m.extendObservable(Object.freeze({}), {})).toThrow(
/Cannot make the designated object observable/
)
@@ -473,7 +473,7 @@ test("540 - extendobservable should not report cycles", function () {
expect(mobx.isObservable(objWrapper.value)).toBeTruthy()
expect(() => {
mobx.extendObservable(objWrapper, objWrapper.value)
- }).toThrowError(/Extending an object with another observable \(object\) is not supported/)
+ }).toThrow(/Extending an object with another observable \(object\) is not supported/)
})
test("mobx 3", () => {
diff --git a/packages/mobx/__tests__/v5/base/map.js b/packages/mobx/__tests__/v5/base/map.js
index e0e0a826aa..c21b5b11dc 100644
--- a/packages/mobx/__tests__/v5/base/map.js
+++ b/packages/mobx/__tests__/v5/base/map.js
@@ -70,7 +70,7 @@ test("map crud", function () {
])
)
expect(JSON.stringify(m)).toMatchInlineSnapshot(
- `"[[\\"1\\",\\"aa\\"],[1,\\"b\\"],[[\\"arr\\"],\\"arrVal\\"],[null,\\"symbol-value\\"]]"`
+ `"[["1","aa"],[1,"b"],[["arr"],"arrVal"],[null,"symbol-value"]]"`
)
expect(m.toString()).toBe("[object ObservableMap]")
expect(m.size).toBe(4)
@@ -915,6 +915,34 @@ test("maps.values, keys and maps.entries are iterables", () => {
expect(Array.from(x.keys())).toEqual(["x", "y"])
})
+// Test support for [iterator-helpers](https://github.com/tc39/proposal-iterator-helpers)
+test("esnext iterator helpers support", () => {
+ const map = mobx.observable(
+ new Map([
+ ["x", [1, 2]],
+ ["y", [3, 4]]
+ ])
+ )
+
+ expect(Array.from(map.keys().map(value => value))).toEqual(["x", "y"])
+ expect(Array.from(map.values().map(value => value))).toEqual([
+ [1, 2],
+ [3, 4]
+ ])
+ expect(Array.from(map.entries().map(([, value]) => value))).toEqual([
+ [1, 2],
+ [3, 4]
+ ])
+
+ expect(Array.from(map.entries().take(1))).toEqual([["x", [1, 2]]])
+ expect(Array.from(map.entries().drop(1))).toEqual([["y", [3, 4]]])
+ expect(Array.from(map.entries().filter(([key]) => key === "y"))).toEqual([["y", [3, 4]]])
+ expect(Array.from(map.entries().find(([key]) => key === "y"))).toEqual(["y", [3, 4]])
+ expect(map.entries().toArray()).toEqual(Array.from(map))
+
+ expect(map.entries().toString()).toEqual("[object MapIterator]")
+})
+
test("toStringTag", () => {
const x = mobx.observable.map({ x: 1, y: 2 })
expect(x[Symbol.toStringTag]).toBe("Map")
diff --git a/packages/mobx/__tests__/v5/base/observables.js b/packages/mobx/__tests__/v5/base/observables.js
index 3d9f67ee6a..a02424dbad 100644
--- a/packages/mobx/__tests__/v5/base/observables.js
+++ b/packages/mobx/__tests__/v5/base/observables.js
@@ -15,7 +15,7 @@ const {
isObservableProp
} = mobx
const utils = require("../../v5/utils/test-utils")
-const { MAX_SPLICE_SIZE } = require("../../../src/internal")
+const { MAX_SPLICE_SIZE, getGlobalState } = require("../../../src/internal")
const voidObserver = function () {}
@@ -1075,7 +1075,7 @@ test("computed values believe deep NaN === deep NaN when using compareStructural
)
const buf = new buffer()
- c.observe_(newValue => {
+ m.observe(c, newValue => {
buf(newValue)
})
@@ -1197,7 +1197,7 @@ test("forcefully tracked reaction should still yield valid results", function ()
transaction(function () {
x.set(4)
a.track(identity)
- expect(a.isScheduled()).toBe(true)
+ expect(a.isScheduled).toBe(true)
expect(z).toBe(4)
expect(runCount).toBe(2)
})
@@ -1207,17 +1207,17 @@ test("forcefully tracked reaction should still yield valid results", function ()
transaction(function () {
x.set(5)
- expect(a.isScheduled()).toBe(true)
+ expect(a.isScheduled).toBe(true)
a.track(identity)
expect(z).toBe(5)
expect(runCount).toBe(3)
- expect(a.isScheduled()).toBe(true)
+ expect(a.isScheduled).toBe(true)
x.set(6)
expect(z).toBe(5)
expect(runCount).toBe(3)
})
- expect(a.isScheduled()).toBe(false)
+ expect(a.isScheduled).toBe(false)
expect(z).toBe(6)
expect(runCount).toBe(4)
})
@@ -1400,6 +1400,49 @@ test("atom events #427", () => {
expect(stop).toBe(2)
})
+test("#3563 reportObserved in batch", () => {
+ let start = 0
+ let stop = 0
+ let computed = 0
+ let observed = 0
+
+ const a = mobx.createAtom(
+ "test",
+ () => start++,
+ () => stop++
+ )
+ const c = mobx.computed(() => {
+ computed += 1
+ observed += a.reportObserved() ? 1 : 0
+ })
+ c.get()
+ expect(start).toBe(0)
+ expect(stop).toBe(0)
+ expect(computed).toBe(1)
+ expect(observed).toBe(0)
+
+ mobx.runInAction(() => {
+ c.get()
+ expect(start).toBe(0)
+ expect(stop).toBe(0)
+ expect(computed).toBe(2)
+ expect(observed).toBe(0)
+
+ c.get()
+ expect(computed).toBe(2)
+ expect(observed).toBe(0)
+ })
+
+ const c2 = mobx.computed(() => {
+ c.get()
+ })
+ c2.get()
+ expect(start).toBe(0)
+ expect(stop).toBe(0)
+ expect(computed).toBe(3)
+ expect(observed).toBe(0)
+})
+
test("verify calculation count", () => {
const calcs = []
const a = observable.box(1)
@@ -1491,7 +1534,7 @@ test("support computed property getters / setters", () => {
a.size = 3
expect(a.volume).toBe(9)
- expect(() => (a.volume = 9)).toThrowError(
+ expect(() => (a.volume = 9)).toThrow(
/It is not possible to assign a new value to a computed value/
)
@@ -1554,7 +1597,7 @@ test("helpful error for self referencing setter", function () {
}
})
- expect(() => (a.y = 2)).toThrowError(/The setter of computed value/)
+ expect(() => (a.y = 2)).toThrow(/The setter of computed value/)
})
test("#558 boxed observables stay boxed observables", function () {
@@ -2317,3 +2360,122 @@ describe("`requiresObservable` takes precedence over global `reactionRequiresObs
expect(consoleWarnSpy).not.toHaveBeenCalled()
})
})
+
+test('Observables initialization does not violate `enforceActions: "always"`', () => {
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {})
+
+ const check = cb => {
+ cb()
+ expect(consoleWarnSpy).not.toHaveBeenCalled()
+ }
+
+ class MakeObservable {
+ x = 0
+ constructor() {
+ mobx.makeObservable(this, { x: true })
+ }
+ }
+ class MakeAutoObservable {
+ x = 0
+ constructor() {
+ mobx.makeAutoObservable(this)
+ }
+ }
+
+ try {
+ mobx.configure({ enforceActions: "always" })
+ check(() => mobx.observable(0))
+ check(() => new MakeObservable())
+ check(() => mobx.makeObservable({ x: 0 }, { x: true }))
+ check(() => new MakeAutoObservable())
+ check(() => mobx.makeAutoObservable({ x: 0 }))
+ check(() => mobx.extendObservable({}, { x: 0 }))
+ check(() => mobx.observable(new Set([0])))
+ check(() => mobx.observable(new Map([[0, 0]])))
+ check(() => mobx.observable({ x: 0 }, { proxy: false }))
+ check(() => mobx.observable({ x: 0 }, { proxy: true }))
+ check(() => mobx.observable([0], { proxy: false }))
+ check(() => mobx.observable([0], { proxy: true }))
+ check(() => mobx.computed(() => 0))
+ } finally {
+ consoleWarnSpy.mockRestore()
+ mobx._resetGlobalState()
+ }
+})
+
+test("enforceAction is respected when changing keys of observable object", () => {
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {})
+ try {
+ mobx.configure({ enforceActions: "always" })
+ const o = mobx.observable({ x: 0 })
+
+ o.y = 0
+ expect(consoleWarnSpy).toHaveBeenCalled()
+
+ consoleWarnSpy.mockClear()
+
+ delete o.x
+ expect(consoleWarnSpy).toHaveBeenCalled()
+ } finally {
+ consoleWarnSpy.mockRestore()
+ mobx._resetGlobalState()
+ }
+})
+
+test("state version does not update on observable creation", () => {
+ const globalState = getGlobalState()
+
+ const check = cb => {
+ const prevStateVersion = globalState.stateVersion
+ cb()
+ expect(prevStateVersion).toBe(globalState.stateVersion)
+ }
+
+ class MakeObservable {
+ x = 0
+ constructor() {
+ mobx.makeObservable(this, { x: true })
+ }
+ }
+ class MakeAutoObservable {
+ x = 0
+ constructor() {
+ mobx.makeAutoObservable(this)
+ }
+ }
+
+ class MakeObservableSubclass extends MakeObservable {
+ y = 0
+ constructor() {
+ super()
+ mobx.makeObservable(this, { y: true })
+ }
+ }
+
+ check(() => mobx.observable(0))
+ check(() => new MakeObservable())
+ // Batch is required by design - without batch reactions can be created/invoked inbetween individual constructors and they must see updated state version.
+ // https://github.com/mobxjs/mobx/pull/3732#discussion_r1285099080
+ check(mobx.action(() => new MakeObservableSubclass()))
+ check(() => mobx.makeObservable({ x: 0 }, { x: true }))
+ check(() => new MakeAutoObservable())
+ check(() => mobx.makeAutoObservable({ x: 0 }))
+ check(() => mobx.extendObservable({}, { x: 0 }))
+ check(() => mobx.observable(new Set([0])))
+ check(() => mobx.observable(new Map([[0, 0]])))
+ check(() => mobx.observable({ x: 0 }, { proxy: false }))
+ check(() => mobx.observable({ x: 0 }, { proxy: true }))
+ check(() => mobx.observable([0], { proxy: false }))
+ check(() => mobx.observable([0], { proxy: true }))
+ check(() => mobx.computed(() => 0))
+})
+
+test("#3747", () => {
+ mobx.runInAction(() => {
+ const o = observable.box(0)
+ const c = computed(() => o.get())
+ expect(c.get()).toBe(0)
+ o.set(1)
+ expect(c.get()).toBe(1) // would fail
+ })
+})
diff --git a/packages/mobx/__tests__/v5/base/observe.ts b/packages/mobx/__tests__/v5/base/observe.ts
index d149763c5e..7f89f36691 100644
--- a/packages/mobx/__tests__/v5/base/observe.ts
+++ b/packages/mobx/__tests__/v5/base/observe.ts
@@ -42,7 +42,7 @@ test("observe computed values", () => {
const f = observable.box(0)
const c = computed(() => v.get())
- c.observe_(e => {
+ observe(c, e => {
v.get()
f.get()
events.push([e.newValue, e.oldValue])
diff --git a/packages/mobx/__tests__/v5/base/reaction.js b/packages/mobx/__tests__/v5/base/reaction.js
index ebb264826d..49f6215db6 100644
--- a/packages/mobx/__tests__/v5/base/reaction.js
+++ b/packages/mobx/__tests__/v5/base/reaction.js
@@ -3,6 +3,7 @@
*/
const mobx = require("../../../src/mobx.ts")
const reaction = mobx.reaction
+const $mobx = mobx.$mobx
const utils = require("../../v5/utils/test-utils")
test("basic", () => {
@@ -260,6 +261,48 @@ test("can dispose reaction on first run", () => {
expect(valuesEffect).toEqual([])
})
+test("can dispose reaction with AbortSignal", () => {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ reaction(
+ () => a.get(),
+ (newValue, oldValue) => {
+ values.push([newValue, oldValue])
+ },
+ { signal: ac.signal }
+ )
+
+ a.set(2)
+ a.set(3)
+ ac.abort()
+ a.set(4)
+
+ expect(values).toEqual([
+ [2, 1],
+ [3, 2]
+ ])
+})
+
+test("fireImmediately should not be honored when passed already aborted AbortSignal", () => {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ ac.abort()
+
+ reaction(
+ () => a.get(),
+ (newValue) => {
+ values.push(newValue)
+ },
+ { signal: ac.signal, fireImmediately: true }
+ )
+
+ expect(values).toEqual([])
+})
+
test("#278 do not rerun if expr output doesn't change", () => {
const a = mobx.observable.box(1)
const values = []
@@ -662,3 +705,27 @@ describe("reaction opts requiresObservable", () => {
expect(messages.length).toBe(0)
})
})
+
+describe("explicit resource management", () => {
+ Symbol.dispose ??= Symbol("Symbol.dispose")
+
+ test("reaction has [Symbol.dispose] and calling it disposes of reaction", () => {
+ const a = mobx.observable.box(1)
+ const values = []
+
+ const disposeReaction = reaction(
+ () => a.get(),
+ newValue => {
+ values.push(newValue)
+ }
+ )
+
+ expect(disposeReaction[Symbol.dispose]).toBeInstanceOf(Function)
+
+ disposeReaction[Symbol.dispose]()
+ a.set(2)
+
+ expect(values).toEqual([])
+ expect(disposeReaction[$mobx].isDisposed).toBe(true)
+ })
+})
diff --git a/packages/mobx/__tests__/v5/base/set.js b/packages/mobx/__tests__/v5/base/set.js
index 04a5ecb57d..c947e73ae0 100644
--- a/packages/mobx/__tests__/v5/base/set.js
+++ b/packages/mobx/__tests__/v5/base/set.js
@@ -201,6 +201,34 @@ test("set should support iterall / iterable ", () => {
expect(leech(a.values())).toEqual([1, 2])
})
+// Test support for [iterator-helpers](https://github.com/tc39/proposal-iterator-helpers)
+test("esnext iterator helpers support", () => {
+ const set = mobx.observable(
+ new Set([
+ [1, 2],
+ [3, 4]
+ ])
+ )
+
+ expect(Array.from(set.values().map(value => value))).toEqual([
+ [1, 2],
+ [3, 4]
+ ])
+
+ expect(Array.from(set.entries().map(([, value]) => value))).toEqual([
+ [1, 2],
+ [3, 4]
+ ])
+
+ expect(Array.from(set.values().take(1))).toEqual([[1, 2]])
+ expect(Array.from(set.values().drop(1))).toEqual([[3, 4]])
+ expect(Array.from(set.values().filter(value => value[0] === 3))).toEqual([[3, 4]])
+ expect(Array.from(set.values().find(value => value[0] === 3))).toEqual([3, 4])
+ expect(Array.from(set.values().flatMap(value => value))).toEqual([1, 2, 3, 4])
+
+ expect(set.entries().toString()).toEqual("[object SetIterator]")
+})
+
test("support for ES6 Set", () => {
const x = new Set()
x.add(1)
@@ -289,3 +317,205 @@ test("set.forEach is reactive", () => {
s.add(2)
expect(c).toBe(3)
})
+
+describe("The Set object methods do what they are supposed to do", () => {
+ const reactiveSet = set([1, 2, 3, 4, 5])
+
+ test("with native Set", () => {
+ const intersectionObservableResult = reactiveSet.intersection(new Set([1, 2, 6]))
+ const unionObservableResult = reactiveSet.union(new Set([1, 2, 6]))
+ const differenceObservableResult = reactiveSet.difference(new Set([1, 2, 3, 4, 5, 6, 7]))
+ const symmetricDifferenceObservableResult = reactiveSet.symmetricDifference(new Set([3, 4]))
+ const isSubsetOfObservableResult = reactiveSet.isSubsetOf(new Set([1, 2, 3]))
+ const isSupersetOfObservableResult = reactiveSet.isSupersetOf(new Set([1, 2, 3, 4, 5, 6]))
+ const isDisjointFromObservableResult = reactiveSet.isDisjointFrom(new Set([6, 7]))
+
+ expect(intersectionObservableResult).toEqual(new Set([1, 2]))
+ expect(unionObservableResult).toEqual(new Set([1, 2, 3, 4, 5, 6]))
+ expect(differenceObservableResult).toEqual(new Set())
+ expect(symmetricDifferenceObservableResult).toEqual(new Set([1, 2, 5]))
+ expect(isSubsetOfObservableResult).toBeFalsy()
+ expect(isSupersetOfObservableResult).toBeFalsy()
+ expect(isDisjointFromObservableResult).toBeTruthy()
+ })
+
+ test("with ObservableSet #3919", () => {
+ const intersectionObservableResult = reactiveSet.intersection(set([1, 2, 6]))
+ const unionObservableResult = reactiveSet.union(set([1, 2, 6]))
+ const differenceObservableResult = reactiveSet.difference(set([1, 2, 3, 4, 5, 6, 7]))
+ const symmetricDifferenceObservableResult = reactiveSet.symmetricDifference(set([3, 4]))
+ const isSubsetOfObservableResult = reactiveSet.isSubsetOf(set([1, 2, 3]))
+ const isSupersetOfObservableResult = reactiveSet.isSupersetOf(set([1, 2, 3, 4, 5, 6]))
+ const isDisjointFromObservableResult = reactiveSet.isDisjointFrom(set([6, 7]))
+
+ expect(intersectionObservableResult).toEqual(new Set([1, 2]))
+ expect(unionObservableResult).toEqual(new Set([1, 2, 3, 4, 5, 6]))
+ expect(differenceObservableResult).toEqual(new Set())
+ expect(symmetricDifferenceObservableResult).toEqual(new Set([1, 2, 5]))
+ expect(isSubsetOfObservableResult).toBeFalsy()
+ expect(isSupersetOfObservableResult).toBeFalsy()
+ expect(isDisjointFromObservableResult).toBeTruthy()
+ })
+
+ test("with Set-like", () => {
+ const intersectionObservableResult = reactiveSet.intersection(
+ new Map([1, 2, 6].map(i => [i, i]))
+ )
+ const unionObservableResult = reactiveSet.union(new Map([1, 2, 6].map(i => [i, i])))
+ const differenceObservableResult = reactiveSet.difference(
+ new Map([1, 2, 3, 4, 5, 6, 7].map(i => [i, i]))
+ )
+ const symmetricDifferenceObservableResult = reactiveSet.symmetricDifference(
+ new Map([3, 4].map(i => [i, i]))
+ )
+ const isSubsetOfObservableResult = reactiveSet.isSubsetOf(
+ new Map([1, 2, 3].map(i => [i, i]))
+ )
+ const isSupersetOfObservableResult = reactiveSet.isSupersetOf(
+ new Map([1, 2, 3, 4, 5, 6].map(i => [i, i]))
+ )
+ const isDisjointFromObservableResult = reactiveSet.isDisjointFrom(
+ new Map([6, 7].map(i => [i, i]))
+ )
+
+ expect(intersectionObservableResult).toEqual(new Set([1, 2]))
+ expect(unionObservableResult).toEqual(new Set([1, 2, 3, 4, 5, 6]))
+ expect(differenceObservableResult).toEqual(new Set())
+ expect(symmetricDifferenceObservableResult).toEqual(new Set([1, 2, 5]))
+ expect(isSubsetOfObservableResult).toBeFalsy()
+ expect(isSupersetOfObservableResult).toBeFalsy()
+ expect(isDisjointFromObservableResult).toBeTruthy()
+ })
+})
+
+describe("Observable Set methods are reactive", () => {
+ let c = 0
+ let s = set()
+
+ beforeEach(() => {
+ c = 0
+ s = set()
+ })
+
+ test("Intersection method is reactive", () => {
+ autorun(() => {
+ s.intersection(new Set())
+ c++
+ })
+
+ s.add(1)
+ s.add(2)
+ expect(c).toBe(3)
+ })
+
+ test("Union method is reactive", () => {
+ autorun(() => {
+ s.union(new Set())
+ c++
+ })
+
+ s.add(1)
+ s.add(2)
+ expect(c).toBe(3)
+ })
+
+ test("Difference method is reactive", () => {
+ autorun(() => {
+ s.difference(new Set())
+ c++
+ })
+
+ s.add(1)
+ s.add(2)
+ expect(c).toBe(3)
+ })
+
+ test("symmetricDifference method is reactive", () => {
+ autorun(() => {
+ s.symmetricDifference(new Set())
+ c++
+ })
+
+ s.add(1)
+ s.add(2)
+ expect(c).toBe(3)
+ })
+
+ test("isSubsetOf method is reactive", () => {
+ autorun(() => {
+ s.isSubsetOf(new Set())
+ c++
+ })
+
+ s.add(1)
+ s.add(2)
+ expect(c).toBe(3)
+ })
+
+ test("isSupersetOf method is reactive", () => {
+ autorun(() => {
+ s.isSupersetOf(new Set())
+ c++
+ })
+
+ s.add(1)
+ s.add(2)
+ expect(c).toBe(3)
+ })
+
+ test("isDisjointFrom method is reactive", () => {
+ autorun(() => {
+ s.isDisjointFrom(new Set())
+ c++
+ })
+
+ s.add(1)
+ s.add(2)
+ expect(c).toBe(3)
+ })
+})
+
+
+describe("Observable Set interceptors", () => {
+
+ let s = set()
+
+ beforeEach(() => {
+ s = set()
+ })
+
+ test("Add does not add value if interceptor returned no change", () => {
+ mobx.intercept(s, (change) => {
+ if(change.type === 'add' && change.newValue === 2) {
+ return undefined;
+ }
+
+ return change;
+ })
+
+ s.add(1);
+ s.add(2);
+
+ expect([...s]).toStrictEqual([1]);
+
+
+ })
+
+ test("Add respects newValue from interceptor", () => {
+
+ mobx.intercept(s, (change) => {
+ if(change.type === 'add' && change.newValue === 2) {
+ change.newValue = 10;
+ }
+
+ return change;
+ })
+
+ s.add(1);
+ s.add(2);
+
+ expect([...s]).toStrictEqual([1, 10])
+ })
+
+
+})
\ No newline at end of file
diff --git a/packages/mobx/__tests__/v5/base/typescript-decorators.ts b/packages/mobx/__tests__/v5/base/typescript-decorators.ts
index 9e6185dc4d..dafbcb3603 100644
--- a/packages/mobx/__tests__/v5/base/typescript-decorators.ts
+++ b/packages/mobx/__tests__/v5/base/typescript-decorators.ts
@@ -22,7 +22,9 @@ import {
IAtom,
createAtom,
runInAction,
- makeObservable
+ makeObservable,
+ flow,
+ flowResult
} from "../../../src/mobx"
import * as mobx from "../../../src/mobx"
@@ -61,6 +63,16 @@ test("decorators", () => {
constructor() {
makeObservable(this)
}
+
+ @flow
+ *testDouble(n: number): Generator {
+ yield 3
+ return n * 2
+ }
+
+ async run() {
+ const x: number = await flowResult(this.testDouble(2))
+ }
}
const o = new Order()
diff --git a/packages/mobx/__tests__/v5/base/typescript-tests.ts b/packages/mobx/__tests__/v5/base/typescript-tests.ts
index c65f66736f..8c17f046dd 100644
--- a/packages/mobx/__tests__/v5/base/typescript-tests.ts
+++ b/packages/mobx/__tests__/v5/base/typescript-tests.ts
@@ -287,6 +287,7 @@ test("computed setter should succeed", () => {
})
test("atom clock example", done => {
+ // TODO randomly fails, rework
let ticks = 0
const time_factor = process.env.CI === "true" ? 300 : 100 // speed up / slow down tests
@@ -1806,6 +1807,36 @@ test("promised when can be cancelled", async () => {
}
})
+test("promised when can be aborted", async () => {
+ const x = mobx.observable.box(1)
+
+ try {
+ const ac = new AbortController()
+ const p = mobx.when(() => x.get() === 3, { signal: ac.signal })
+ setTimeout(() => ac.abort(), 100)
+ await p
+ fail("should abort")
+ } catch (e) {
+ expect("" + e).toMatch(/WHEN_ABORTED/)
+ }
+})
+
+test("sync when can be aborted", async () => {
+ const x = mobx.observable.box(1)
+
+ const ac = new AbortController()
+ mobx.when(
+ () => x.get() === 3,
+ () => {
+ fail("should abort")
+ },
+ { signal: ac.signal }
+ )
+ ac.abort()
+
+ x.set(3)
+})
+
test("it should support asyncAction as decorator (ts)", async () => {
mobx.configure({ enforceActions: "observed" })
@@ -1887,6 +1918,59 @@ test("flow support throwing async generators", async () => {
}
})
+test("allow 'as const' for creating tuples for map.replace()", () => {
+ const map = mobx.observable.map()
+ const srcValues = mobx.observable.array()
+ const newValues = Array.from(srcValues, (value, index) => [value, index] as const)
+
+ map.replace(newValues)
+})
+
+test("allow 'as const' for creating tuples for observable.map()", () => {
+ const srcValues = mobx.observable.array()
+ const newValues = Array.from(srcValues, (value, index) => [value, index] as const)
+
+ mobx.observable.map(newValues)
+})
+
+test("infers tuple type for observable.map() when using array.map", () => {
+ interface Usage {
+ daily: number
+ monthly: number
+ }
+ const data: Array<{ id: string; usage: Usage }> = [
+ { id: "1", usage: { daily: 5, monthly: 100 } },
+ { id: "2", usage: { daily: 10, monthly: 200 } },
+ { id: "3", usage: { daily: 15, monthly: 300 } }
+ ]
+
+ const map = mobx.observable.map(
+ data.map(app => [app.id, app.usage]),
+ { name: "test" }
+ )
+
+ expect(map.get("2")).toEqual({ daily: 10, monthly: 200 })
+})
+
+test("observable.map() accepts an undefined value with or without options", () => {
+ const map = mobx.observable.map(undefined, { name: "custom name" })
+ map.set("test", 1)
+ expect(map.get("test")).toBe(1)
+ expect(map.name_).toBe("custom name")
+
+ // without options
+ mobx.observable.map(undefined)
+})
+
+test("observable.map() accepts optional initial values", () => {
+ interface Usage {}
+
+ ;(data?: [string, Usage][]) => observable.map(data, { name: "test" })
+ ;(data?: readonly (readonly [string, Usage])[]) => observable.map(data, { name: "test" })
+ ;(data?: Record) => observable.map(data, { name: "test" })
+ ;(data?: Map) => observable.map(data, { name: "test" })
+})
+
test("toJS bug #1413 (TS)", () => {
class X {
test = {
@@ -2413,3 +2497,18 @@ test("#2485", () => {
expect(new Color(1, 2, 3, 4).toString()).toMatchInlineSnapshot(`"rgba(1, 2, 3, 4)"`)
})
+
+test("argumentless observable adds undefined to the output type", () => {
+ const a = observable.box()
+ assert>>(true)
+})
+
+test("with initial value observable does not adds undefined to the output type", () => {
+ const a = observable.box("hello")
+ assert>>(true)
+})
+
+test("observable.box should keep track of undefined and null in type", () => {
+ const a = observable.box()
+ assert>>(true)
+})
diff --git a/packages/mobx/jest.config-decorators.js b/packages/mobx/jest.config-decorators.js
new file mode 100644
index 0000000000..751c550a4f
--- /dev/null
+++ b/packages/mobx/jest.config-decorators.js
@@ -0,0 +1,11 @@
+const path = require("path")
+const buildConfig = require("../../jest.base.config")
+
+module.exports = buildConfig(
+ __dirname,
+ {
+ testRegex: "__tests__/decorators_20223/.*\\.(t|j)sx?$",
+ setupFilesAfterEnv: [`/jest.setup.ts`]
+ },
+ path.resolve(__dirname, "./__tests__/decorators_20223/tsconfig.json")
+)
diff --git a/packages/mobx/jest.projects.js b/packages/mobx/jest.projects.js
new file mode 100644
index 0000000000..6ded2bb72a
--- /dev/null
+++ b/packages/mobx/jest.projects.js
@@ -0,0 +1,6 @@
+module.exports = {
+ collectCoverageFrom: ["/src/**/*.{ts,tsx}", "!**/node_modules/**"],
+ coverageDirectory: "/coverage/",
+ coverageReporters: ["lcov", "text"],
+ projects: ["/jest.config.js", "/jest.config-decorators.js"]
+}
diff --git a/packages/mobx/jest.setup.ts b/packages/mobx/jest.setup.ts
index d7ff3472ce..74251d4e79 100644
--- a/packages/mobx/jest.setup.ts
+++ b/packages/mobx/jest.setup.ts
@@ -1,5 +1,7 @@
import { configure, _resetGlobalState } from "./src/mobx"
+global.setImmediate = global.setImmediate || ((fn, ...args) => global.setTimeout(fn, 0, ...args))
+
beforeEach(() => {
// @ts-ignore
global.__DEV__ = true
diff --git a/packages/mobx/package.json b/packages/mobx/package.json
index 877f4ac7d7..c6eaa42d90 100644
--- a/packages/mobx/package.json
+++ b/packages/mobx/package.json
@@ -1,6 +1,6 @@
{
"name": "mobx",
- "version": "6.5.0",
+ "version": "6.15.1",
"description": "Simple, scalable state management.",
"source": "src/mobx.ts",
"main": "dist/index.js",
@@ -61,7 +61,7 @@
"data flow"
],
"scripts": {
- "test": "jest",
+ "test": "jest --config jest.projects.js",
"lint": "eslint src/**/*",
"build": "node ../../scripts/build.js mobx",
"build:test": "yarn build --target test",
@@ -75,6 +75,6 @@
"test:coverage": "yarn test -i --coverage",
"test:size": "yarn import-size --report . observable computed autorun action",
"test:check": "yarn test:types",
- "prepublish": "node ./scripts/prepublish.js && yarn build --target publish"
+ "prepublishOnly": "node ./scripts/prepublish.js && yarn build --target publish"
}
}
diff --git a/packages/mobx/src/api/action.ts b/packages/mobx/src/api/action.ts
index ab5e386011..2fc2b519e7 100644
--- a/packages/mobx/src/api/action.ts
+++ b/packages/mobx/src/api/action.ts
@@ -7,9 +7,12 @@ import {
isFunction,
isStringish,
createDecoratorAnnotation,
- createActionAnnotation
+ createActionAnnotation,
+ is20223Decorator
} from "../internal"
+import type { ClassFieldDecorator, ClassMethodDecorator } from "../types/decorator_fills"
+
export const ACTION = "action"
export const ACTION_BOUND = "action.bound"
export const AUTOACTION = "autoAction"
@@ -29,17 +32,24 @@ const autoActionBoundAnnotation = createActionAnnotation(AUTOACTION_BOUND, {
bound: true
})
-export interface IActionFactory extends Annotation, PropertyDecorator {
+export interface IActionFactory
+ extends Annotation,
+ PropertyDecorator,
+ ClassMethodDecorator,
+ ClassFieldDecorator {
// nameless actions
(fn: T): T
// named actions
(name: string, fn: T): T
// named decorator
- (customName: string): PropertyDecorator & Annotation
+ (customName: string): PropertyDecorator &
+ Annotation &
+ ClassMethodDecorator &
+ ClassFieldDecorator
// decorator (name no longer supported)
- bound: Annotation & PropertyDecorator
+ bound: Annotation & PropertyDecorator & ClassMethodDecorator & ClassFieldDecorator
}
function createActionFactory(autoAction: boolean): IActionFactory {
@@ -52,6 +62,13 @@ function createActionFactory(autoAction: boolean): IActionFactory {
if (isFunction(arg2)) {
return createAction(arg1, arg2, autoAction)
}
+ // @action (2022.3 Decorators)
+ if (is20223Decorator(arg2)) {
+ return (autoAction ? autoActionAnnotation : actionAnnotation).decorate_20223_(
+ arg1,
+ arg2
+ )
+ }
// @action
if (isStringish(arg2)) {
return storeAnnotation(arg1, arg2, autoAction ? autoActionAnnotation : actionAnnotation)
diff --git a/packages/mobx/src/api/annotation.ts b/packages/mobx/src/api/annotation.ts
index 87a9ea607f..719c727402 100644
--- a/packages/mobx/src/api/annotation.ts
+++ b/packages/mobx/src/api/annotation.ts
@@ -20,6 +20,7 @@ export type Annotation = {
descriptor: PropertyDescriptor,
proxyTrap: boolean
): boolean | null
+ decorate_20223_(value: any, context: DecoratorContext)
options_?: any
}
diff --git a/packages/mobx/src/api/autorun.ts b/packages/mobx/src/api/autorun.ts
index 1ddc23e4b2..63454ab169 100644
--- a/packages/mobx/src/api/autorun.ts
+++ b/packages/mobx/src/api/autorun.ts
@@ -12,7 +12,8 @@ import {
isFunction,
isPlainObject,
die,
- allowStateChanges
+ allowStateChanges,
+ GenericAbortSignal
} from "../internal"
export interface IAutorunOptions {
@@ -25,6 +26,7 @@ export interface IAutorunOptions {
requiresObservable?: boolean
scheduler?: (callback: () => void) => any
onError?: (error: any) => void
+ signal?: GenericAbortSignal
}
/**
@@ -73,7 +75,7 @@ export function autorun(
isScheduled = true
scheduler(() => {
isScheduled = false
- if (!reaction.isDisposed_) {
+ if (!reaction.isDisposed) {
reaction.track(reactionRunner)
}
})
@@ -88,8 +90,10 @@ export function autorun(
view(reaction)
}
- reaction.schedule_()
- return reaction.getDisposer_()
+ if (!opts?.signal?.aborted) {
+ reaction.schedule_()
+ }
+ return reaction.getDisposer_(opts?.signal)
}
export type IReactionOptions = IAutorunOptions & {
@@ -135,7 +139,6 @@ export function reaction(
let firstTime = true
let isScheduled = false
let value: T
- let oldValue: T | undefined
const equals: IEqualsComparer = (opts as any).compareStructural
? comparer.structural
@@ -157,18 +160,18 @@ export function reaction(
function reactionRunner() {
isScheduled = false
- if (r.isDisposed_) {
+ if (r.isDisposed) {
return
}
let changed: boolean = false
+ const oldValue = value
r.track(() => {
const nextValue = allowStateChanges(false, () => expression(r))
changed = firstTime || !equals(value, nextValue)
- oldValue = value
value = nextValue
})
- // This casting is nesessary as TS cannot infer proper type in current funciton implementation
+ // This casting is nesessary as TS cannot infer proper type in current function implementation
type OldValue = FireImmediately extends true ? T | undefined : T
if (firstTime && opts.fireImmediately!) {
effectAction(value, oldValue as OldValue, r)
@@ -178,8 +181,10 @@ export function reaction(
firstTime = false
}
- r.schedule_()
- return r.getDisposer_()
+ if (!opts?.signal?.aborted) {
+ r.schedule_()
+ }
+ return r.getDisposer_(opts?.signal)
}
function wrapErrorHandler(errorHandler, baseFn) {
diff --git a/packages/mobx/src/api/computed.ts b/packages/mobx/src/api/computed.ts
index 104932bd38..6ac31e8e04 100644
--- a/packages/mobx/src/api/computed.ts
+++ b/packages/mobx/src/api/computed.ts
@@ -10,19 +10,22 @@ import {
die,
IComputedValue,
createComputedAnnotation,
- comparer
+ comparer,
+ is20223Decorator
} from "../internal"
+import type { ClassGetterDecorator } from "../types/decorator_fills"
+
export const COMPUTED = "computed"
export const COMPUTED_STRUCT = "computed.struct"
-export interface IComputedFactory extends Annotation, PropertyDecorator {
+export interface IComputedFactory extends Annotation, PropertyDecorator, ClassGetterDecorator {
// @computed(opts)
- (options: IComputedValueOptions): Annotation & PropertyDecorator
+ (options: IComputedValueOptions): Annotation & PropertyDecorator & ClassGetterDecorator
// computed(fn, opts)
(func: () => T, options?: IComputedValueOptions): IComputedValue
- struct: Annotation & PropertyDecorator
+ struct: Annotation & PropertyDecorator & ClassGetterDecorator
}
const computedAnnotation = createComputedAnnotation(COMPUTED)
@@ -35,6 +38,10 @@ const computedStructAnnotation = createComputedAnnotation(COMPUTED_STRUCT, {
* For legacy purposes also invokable as ES5 observable created: `computed(() => expr)`;
*/
export const computed: IComputedFactory = function computed(arg1, arg2) {
+ if (is20223Decorator(arg2)) {
+ // @computed (2022.3 Decorators)
+ return computedAnnotation.decorate_20223_(arg1, arg2)
+ }
if (isStringish(arg2)) {
// @computed
return storeAnnotation(arg1, arg2, computedAnnotation)
diff --git a/packages/mobx/src/api/decorators.ts b/packages/mobx/src/api/decorators.ts
index a88def3aa6..9ecde78c12 100644
--- a/packages/mobx/src/api/decorators.ts
+++ b/packages/mobx/src/api/decorators.ts
@@ -1,5 +1,7 @@
import { Annotation, addHiddenProp, AnnotationsMap, hasProp, die, isOverride } from "../internal"
+import type { Decorator } from "../types/decorator_fills"
+
export const storedAnnotationsSymbol = Symbol("mobx-stored-annotations")
/**
@@ -7,11 +9,17 @@ export const storedAnnotationsSymbol = Symbol("mobx-stored-annotations")
* - decorator
* - annotation object
*/
-export function createDecoratorAnnotation(annotation: Annotation): PropertyDecorator & Annotation {
+export function createDecoratorAnnotation(
+ annotation: Annotation
+): PropertyDecorator & Annotation & D {
function decorator(target, property) {
- storeAnnotation(target, property, annotation)
+ if (is20223Decorator(property)) {
+ return annotation.decorate_20223_(target, property)
+ } else {
+ storeAnnotation(target, property, annotation)
+ }
}
- return Object.assign(decorator, annotation)
+ return Object.assign(decorator, annotation) as any
}
/**
@@ -51,7 +59,7 @@ function assertNotDecorated(prototype: object, annotation: Annotation, key: Prop
`Cannot apply '@${requestedAnnotationType}' to '${fieldName}':` +
`\nThe field is already decorated with '@${currentAnnotationType}'.` +
`\nRe-decorating fields is not allowed.` +
- `\nUse '@override' decorator for methods overriden by subclass.`
+ `\nUse '@override' decorator for methods overridden by subclass.`
)
}
}
@@ -61,13 +69,30 @@ function assertNotDecorated(prototype: object, annotation: Annotation, key: Prop
*/
export function collectStoredAnnotations(target): AnnotationsMap {
if (!hasProp(target, storedAnnotationsSymbol)) {
- if (__DEV__ && !target[storedAnnotationsSymbol]) {
- die(
- `No annotations were passed to makeObservable, but no decorated members have been found either`
- )
- }
+ // if (__DEV__ && !target[storedAnnotationsSymbol]) {
+ // die(
+ // `No annotations were passed to makeObservable, but no decorated members have been found either`
+ // )
+ // }
// We need a copy as we will remove annotation from the list once it's applied.
addHiddenProp(target, storedAnnotationsSymbol, { ...target[storedAnnotationsSymbol] })
}
return target[storedAnnotationsSymbol]
}
+
+export function is20223Decorator(context): context is DecoratorContext {
+ return typeof context == "object" && typeof context["kind"] == "string"
+}
+
+export function assert20223DecoratorType(
+ context: DecoratorContext,
+ types: DecoratorContext["kind"][]
+) {
+ if (__DEV__ && !types.includes(context.kind)) {
+ die(
+ `The decorator applied to '${String(context.name)}' cannot be used on a ${
+ context.kind
+ } element`
+ )
+ }
+}
diff --git a/packages/mobx/src/api/extendobservable.ts b/packages/mobx/src/api/extendobservable.ts
index 7b223152a3..44a81f916a 100644
--- a/packages/mobx/src/api/extendobservable.ts
+++ b/packages/mobx/src/api/extendobservable.ts
@@ -2,8 +2,6 @@ import {
CreateObservableOptions,
isObservableMap,
AnnotationsMap,
- startBatch,
- endBatch,
asObservableObject,
isPlainObject,
ObservableObjectAdministration,
@@ -11,7 +9,8 @@ import {
die,
getOwnPropertyDescriptors,
$mobx,
- ownKeys
+ ownKeys,
+ initObservable
} from "../internal"
export function extendObservable(
@@ -40,9 +39,8 @@ export function extendObservable(
// Pull descriptors first, so we don't have to deal with props added by administration ($mobx)
const descriptors = getOwnPropertyDescriptors(properties)
- const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
- startBatch()
- try {
+ initObservable(() => {
+ const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
ownKeys(descriptors).forEach(key => {
adm.extend_(
key,
@@ -51,8 +49,7 @@ export function extendObservable(
!annotations ? true : key in annotations ? annotations[key] : true
)
})
- } finally {
- endBatch()
- }
+ })
+
return target as any
}
diff --git a/packages/mobx/src/api/flow.ts b/packages/mobx/src/api/flow.ts
index daad7a7e5a..43b00715c6 100644
--- a/packages/mobx/src/api/flow.ts
+++ b/packages/mobx/src/api/flow.ts
@@ -7,17 +7,27 @@ import {
isStringish,
storeAnnotation,
createFlowAnnotation,
- createDecoratorAnnotation
+ createDecoratorAnnotation,
+ is20223Decorator
} from "../internal"
+import type { ClassMethodDecorator } from "../types/decorator_fills"
+
export const FLOW = "flow"
let generatorId = 0
-export function FlowCancellationError() {
- this.message = "FLOW_CANCELLED"
+export class FlowCancellationError extends Error {
+ constructor() {
+ super("FLOW_CANCELLED")
+ Object.setPrototypeOf(this, new.target.prototype)
+ this.name = "FlowCancellationError"
+ }
+
+ toString() {
+ return `Error: ${this.message}`
+ }
}
-FlowCancellationError.prototype = Object.create(Error.prototype)
export function isFlowCancellationError(error: Error) {
return error instanceof FlowCancellationError
@@ -25,11 +35,11 @@ export function isFlowCancellationError(error: Error) {
export type CancellablePromise = Promise & { cancel(): void }
-interface Flow extends Annotation, PropertyDecorator {
+interface Flow extends Annotation, PropertyDecorator, ClassMethodDecorator {
(
generator: (...args: Args) => Generator | AsyncGenerator
): (...args: Args) => CancellablePromise
- bound: Annotation & PropertyDecorator
+ bound: Annotation & PropertyDecorator & ClassMethodDecorator
}
const flowAnnotation = createFlowAnnotation("flow")
@@ -37,6 +47,10 @@ const flowBoundAnnotation = createFlowAnnotation("flow.bound", { bound: true })
export const flow: Flow = Object.assign(
function flow(arg1, arg2?) {
+ // @flow (2022.3 Decorators)
+ if (is20223Decorator(arg2)) {
+ return flowAnnotation.decorate_20223_(arg1, arg2)
+ }
// @flow
if (isStringish(arg2)) {
return storeAnnotation(arg1, arg2, flowAnnotation)
diff --git a/packages/mobx/src/api/makeObservable.ts b/packages/mobx/src/api/makeObservable.ts
index afdd49d7cb..156161d241 100644
--- a/packages/mobx/src/api/makeObservable.ts
+++ b/packages/mobx/src/api/makeObservable.ts
@@ -2,8 +2,6 @@ import {
$mobx,
asObservableObject,
AnnotationsMap,
- endBatch,
- startBatch,
CreateObservableOptions,
ObservableObjectAdministration,
collectStoredAnnotations,
@@ -13,7 +11,8 @@ import {
ownKeys,
extendObservable,
addHiddenProp,
- storedAnnotationsSymbol
+ storedAnnotationsSymbol,
+ initObservable
} from "../internal"
// Hack based on https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-322267089
@@ -23,14 +22,15 @@ import {
// Fixes: https://github.com/mobxjs/mobx/issues/2325#issuecomment-691070022
type NoInfer = [T][T extends any ? 0 : never]
+type MakeObservableOptions = Omit
+
export function makeObservable(
target: T,
annotations?: AnnotationsMap>,
- options?: CreateObservableOptions
+ options?: MakeObservableOptions
): T {
- const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
- startBatch()
- try {
+ initObservable(() => {
+ const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
if (__DEV__ && annotations && target[storedAnnotationsSymbol]) {
die(
`makeObservable second arg must be nullish when using decorators. Mixing @decorator syntax with annotations is not supported.`
@@ -41,9 +41,7 @@ export function makeObservable adm.make_(key, annotations![key]))
- } finally {
- endBatch()
- }
+ })
return target
}
@@ -53,7 +51,7 @@ const keysSymbol = Symbol("mobx-keys")
export function makeAutoObservable(
target: T,
overrides?: AnnotationsMap>,
- options?: CreateObservableOptions
+ options?: MakeObservableOptions
): T {
if (__DEV__) {
if (!isPlainObject(target) && !isPlainObject(Object.getPrototypeOf(target))) {
@@ -70,20 +68,19 @@ export function makeAutoObservable {
+ const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
- // Optimization: cache keys on proto
- // Assumes makeAutoObservable can be called only once per object and can't be used in subclass
- if (!target[keysSymbol]) {
- const proto = Object.getPrototypeOf(target)
- const keys = new Set([...ownKeys(target), ...ownKeys(proto)])
- keys.delete("constructor")
- keys.delete($mobx)
- addHiddenProp(proto, keysSymbol, keys)
- }
+ // Optimization: cache keys on proto
+ // Assumes makeAutoObservable can be called only once per object and can't be used in subclass
+ if (!target[keysSymbol]) {
+ const proto = Object.getPrototypeOf(target)
+ const keys = new Set([...ownKeys(target), ...ownKeys(proto)])
+ keys.delete("constructor")
+ keys.delete($mobx)
+ addHiddenProp(proto, keysSymbol, keys)
+ }
- startBatch()
- try {
target[keysSymbol].forEach(key =>
adm.make_(
key,
@@ -91,8 +88,7 @@ export function makeAutoObservable(observableAnnotation)
export function getEnhancerFromOptions(options: CreateObservableOptions): IEnhancer {
return options.deep === true
@@ -95,6 +103,11 @@ export function getEnhancerFromAnnotation(annotation?: Annotation): IEnhancer(value: T, options?: CreateObservableOptions): IObservableValue
+ (value?: T, options?: CreateObservableOptions): IObservableValue
+}
-export interface IObservableFactory extends Annotation, PropertyDecorator {
+export interface IObservableMapFactory {
+ (): ObservableMap
+ (initialValues?: IMapEntries, options?: CreateObservableOptions): ObservableMap<
+ K,
+ V
+ >
+ (
+ initialValues?: IReadonlyMapEntries,
+ options?: CreateObservableOptions
+ ): ObservableMap
+ (initialValues?: IKeyValueMap, options?: CreateObservableOptions): ObservableMap
+ (initialValues?: Map, options?: CreateObservableOptions): ObservableMap
+ (initialValues: undefined, options?: CreateObservableOptions): ObservableMap<
+ K,
+ V
+ >
+}
+
+export interface IObservableFactory
+ extends Annotation,
+ PropertyDecorator,
+ ClassAccessorDecorator,
+ ClassFieldDecorator {
+ // TODO: remove ClassFieldDecorator, this is only temporarily support for legacy decorators
(value: T[], options?: CreateObservableOptions): IObservableArray
(value: Set, options?: CreateObservableOptions): ObservableSet
(value: Map, options?: CreateObservableOptions): ObservableMap
@@ -146,16 +187,13 @@ export interface IObservableFactory extends Annotation, PropertyDecorator {
options?: CreateObservableOptions
): T
- box: (value?: T, options?: CreateObservableOptions) => IObservableValue
+ box: IObservableValueFactory
array: (initialValues?: T[], options?: CreateObservableOptions) => IObservableArray
set: (
initialValues?: IObservableSetInitialValues,
options?: CreateObservableOptions
) => ObservableSet
- map: (
- initialValues?: IObservableMapInitialValues,
- options?: CreateObservableOptions
- ) => ObservableMap
+ map: IObservableMapFactory
object: (
props: T,
decorators?: AnnotationsMap,
@@ -165,17 +203,17 @@ export interface IObservableFactory extends Annotation, PropertyDecorator {
/**
* Decorator that creates an observable that only observes the references, but doesn't try to turn the assigned value into an observable.ts.
*/
- ref: Annotation & PropertyDecorator
+ ref: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
/**
* Decorator that creates an observable converts its value (objects, maps or arrays) into a shallow observable structure
*/
- shallow: Annotation & PropertyDecorator
- deep: Annotation & PropertyDecorator
- struct: Annotation & PropertyDecorator
+ shallow: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
+ deep: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
+ struct: Annotation & PropertyDecorator & ClassAccessorDecorator & ClassFieldDecorator
}
const observableFactories: IObservableFactory = {
- box(value?: T, options?: CreateObservableOptions): IObservableValue {
+ box(value: T, options?: CreateObservableOptions): IObservableValue {
const o = asCreateObservableOptions(options)
return new ObservableValue(value, getEnhancerFromOptions(o), o.name, true, o.equals)
},
@@ -201,17 +239,19 @@ const observableFactories: IObservableFactory = {
const o = asCreateObservableOptions(options)
return new ObservableSet(initialValues, getEnhancerFromOptions(o), o.name)
},
- object(
+ object(
props: T,
decorators?: AnnotationsMap,
options?: CreateObservableOptions
): T {
- return extendObservable(
- globalState.useProxies === false || options?.proxy === false
- ? asObservableObject({}, options)
- : asDynamicObservableObject({}, options),
- props,
- decorators
+ return initObservable(() =>
+ extendObservable(
+ globalState.useProxies === false || options?.proxy === false
+ ? asObservableObject({}, options)
+ : asDynamicObservableObject({}, options),
+ props,
+ decorators
+ )
)
},
ref: createDecoratorAnnotation(observableRefAnnotation),
diff --git a/packages/mobx/src/api/trace.ts b/packages/mobx/src/api/trace.ts
index 297014f54b..c304329b3d 100644
--- a/packages/mobx/src/api/trace.ts
+++ b/packages/mobx/src/api/trace.ts
@@ -5,7 +5,7 @@ export function trace(thing?: any, enterBreakPoint?: boolean): void
export function trace(enterBreakPoint?: boolean): void
export function trace(...args: any[]): void {
if (!__DEV__) {
- die(`trace() is not available in production builds`)
+ return
}
let enterBreakPoint = false
if (typeof args[args.length - 1] === "boolean") {
diff --git a/packages/mobx/src/api/when.ts b/packages/mobx/src/api/when.ts
index 4c0439635e..66985a1af6 100644
--- a/packages/mobx/src/api/when.ts
+++ b/packages/mobx/src/api/when.ts
@@ -6,13 +6,15 @@ import {
createAction,
getNextId,
die,
- allowStateChanges
+ allowStateChanges,
+ GenericAbortSignal
} from "../internal"
export interface IWhenOptions {
name?: string
timeout?: number
onError?: (error: any) => void
+ signal?: GenericAbortSignal
}
export function when(
@@ -36,7 +38,7 @@ function _when(predicate: () => boolean, effect: Lambda, opts: IWhenOptions): IR
if (typeof opts.timeout === "number") {
const error = new Error("WHEN_TIMEOUT")
timeoutHandle = setTimeout(() => {
- if (!disposer[$mobx].isDisposed_) {
+ if (!disposer[$mobx].isDisposed) {
disposer()
if (opts.onError) {
opts.onError(error)
@@ -74,14 +76,23 @@ function whenPromise(
if (__DEV__ && opts && opts.onError) {
return die(`the options 'onError' and 'promise' cannot be combined`)
}
+ if (opts?.signal?.aborted) {
+ return Object.assign(Promise.reject(new Error("WHEN_ABORTED")), { cancel: () => null })
+ }
let cancel
+ let abort
const res = new Promise((resolve, reject) => {
let disposer = _when(predicate, resolve as Lambda, { ...opts, onError: reject })
cancel = () => {
disposer()
reject(new Error("WHEN_CANCELLED"))
}
- })
+ abort = () => {
+ disposer()
+ reject(new Error("WHEN_ABORTED"))
+ }
+ opts?.signal?.addEventListener?.("abort", abort)
+ }).finally(() => opts?.signal?.removeEventListener?.("abort", abort))
;(res as any).cancel = cancel
return res as any
}
diff --git a/packages/mobx/src/core/action.ts b/packages/mobx/src/core/action.ts
index 22fde33de7..ec8ab01e63 100644
--- a/packages/mobx/src/core/action.ts
+++ b/packages/mobx/src/core/action.ts
@@ -14,7 +14,8 @@ import {
ACTION,
EMPTY_ARRAY,
die,
- getDescriptor
+ getDescriptor,
+ defineProperty
} from "../internal"
// we don't use globalState for these in order to avoid possible issues with multiple
@@ -49,9 +50,10 @@ export function createAction(
return executeAction(actionName, autoAction, fn, ref || this, arguments)
}
res.isMobxAction = true
+ res.toString = () => fn.toString()
if (isFunctionNameConfigurable) {
tmpNameDescriptor.value = actionName
- Object.defineProperty(res, "name", tmpNameDescriptor)
+ defineProperty(res, "name", tmpNameDescriptor)
}
return res
}
diff --git a/packages/mobx/src/core/atom.ts b/packages/mobx/src/core/atom.ts
index c62cbd7036..2df57def5c 100644
--- a/packages/mobx/src/core/atom.ts
+++ b/packages/mobx/src/core/atom.ts
@@ -14,19 +14,23 @@ import {
Lambda
} from "../internal"
+import { getFlag, setFlag } from "../utils/utils"
+
export const $mobx = Symbol("mobx administration")
export interface IAtom extends IObservable {
reportObserved(): boolean
- reportChanged()
+ reportChanged(): void
}
export class Atom implements IAtom {
- isPendingUnobservation_ = false // for effective unobserving. BaseAtom has true, for extra optimization, so its onBecomeUnobserved never gets called, because it's not needed
- isBeingObserved_ = false
+ private static readonly isBeingObservedMask_ = 0b001
+ private static readonly isPendingUnobservationMask_ = 0b010
+ private static readonly diffValueMask_ = 0b100
+ private flags_ = 0b000
+
observers_ = new Set()
- diffValue_ = 0
lastAccessedBy_ = 0
lowestObserverState_ = IDerivationState_.NOT_TRACKING_
/**
@@ -35,6 +39,28 @@ export class Atom implements IAtom {
*/
constructor(public name_ = __DEV__ ? "Atom@" + getNextId() : "Atom") {}
+ // for effective unobserving. BaseAtom has true, for extra optimization, so its onBecomeUnobserved never gets called, because it's not needed
+ get isBeingObserved(): boolean {
+ return getFlag(this.flags_, Atom.isBeingObservedMask_)
+ }
+ set isBeingObserved(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, Atom.isBeingObservedMask_, newValue)
+ }
+
+ get isPendingUnobservation(): boolean {
+ return getFlag(this.flags_, Atom.isPendingUnobservationMask_)
+ }
+ set isPendingUnobservation(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, Atom.isPendingUnobservationMask_, newValue)
+ }
+
+ get diffValue(): 0 | 1 {
+ return getFlag(this.flags_, Atom.diffValueMask_) ? 1 : 0
+ }
+ set diffValue(newValue: 0 | 1) {
+ this.flags_ = setFlag(this.flags_, Atom.diffValueMask_, newValue === 1 ? true : false)
+ }
+
// onBecomeObservedListeners
public onBOL: Set | undefined
// onBecomeUnobservedListeners
diff --git a/packages/mobx/src/core/computedvalue.ts b/packages/mobx/src/core/computedvalue.ts
index 264a1918d8..cec036d6c1 100644
--- a/packages/mobx/src/core/computedvalue.ts
+++ b/packages/mobx/src/core/computedvalue.ts
@@ -32,10 +32,11 @@ import {
allowStateChangesEnd
} from "../internal"
+import { getFlag, setFlag } from "../utils/utils"
+
export interface IComputedValue {
get(): T
set(value: T): void
- observe_(listener: (change: IComputedDidChange) => void, fireImmediately?: boolean): Lambda
}
export interface IComputedValueOptions {
@@ -80,10 +81,7 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva
dependenciesState_ = IDerivationState_.NOT_TRACKING_
observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes
newObserving_ = null // during tracking it's an array with new observed observers
- isBeingObserved_ = false
- isPendingUnobservation_: boolean = false
observers_ = new Set()
- diffValue_ = 0
runId_ = 0
lastAccessedBy_ = 0
lowestObserverState_ = IDerivationState_.UP_TO_DATE_
@@ -91,8 +89,14 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva
protected value_: T | undefined | CaughtException = new CaughtException(null)
name_: string
triggeredBy_?: string
- isComputing_: boolean = false // to check for cycles
- isRunningSetter_: boolean = false
+
+ private static readonly isComputingMask_ = 0b00001
+ private static readonly isRunningSetterMask_ = 0b00010
+ private static readonly isBeingObservedMask_ = 0b00100
+ private static readonly isPendingUnobservationMask_ = 0b01000
+ private static readonly diffValueMask_ = 0b10000
+ private flags_ = 0b00000
+
derivation: () => T // N.B: unminified as it is used by MST
setter_?: (value: T) => void
isTracing_: TraceMode = TraceMode.NONE
@@ -154,12 +158,52 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva
}
}
+ // to check for cycles
+ private get isComputing(): boolean {
+ return getFlag(this.flags_, ComputedValue.isComputingMask_)
+ }
+ private set isComputing(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, ComputedValue.isComputingMask_, newValue)
+ }
+
+ private get isRunningSetter(): boolean {
+ return getFlag(this.flags_, ComputedValue.isRunningSetterMask_)
+ }
+ private set isRunningSetter(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, ComputedValue.isRunningSetterMask_, newValue)
+ }
+
+ get isBeingObserved(): boolean {
+ return getFlag(this.flags_, ComputedValue.isBeingObservedMask_)
+ }
+ set isBeingObserved(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, ComputedValue.isBeingObservedMask_, newValue)
+ }
+
+ get isPendingUnobservation(): boolean {
+ return getFlag(this.flags_, ComputedValue.isPendingUnobservationMask_)
+ }
+ set isPendingUnobservation(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, ComputedValue.isPendingUnobservationMask_, newValue)
+ }
+
+ get diffValue(): 0 | 1 {
+ return getFlag(this.flags_, ComputedValue.diffValueMask_) ? 1 : 0
+ }
+ set diffValue(newValue: 0 | 1) {
+ this.flags_ = setFlag(
+ this.flags_,
+ ComputedValue.diffValueMask_,
+ newValue === 1 ? true : false
+ )
+ }
+
/**
* Returns the current value of this computed value.
* Will evaluate its computation first if needed.
*/
public get(): T {
- if (this.isComputing_) {
+ if (this.isComputing) {
die(32, this.name_, this.derivation)
}
if (
@@ -197,14 +241,14 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva
public set(value: T) {
if (this.setter_) {
- if (this.isRunningSetter_) {
+ if (this.isRunningSetter) {
die(33, this.name_)
}
- this.isRunningSetter_ = true
+ this.isRunningSetter = true
try {
this.setter_.call(this.scope_, value)
} finally {
- this.isRunningSetter_ = false
+ this.isRunningSetter = false
}
} else {
die(34, this.name_)
@@ -243,7 +287,7 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva
}
computeValue_(track: boolean) {
- this.isComputing_ = true
+ this.isComputing = true
// don't allow state changes during computation
const prev = allowStateChangesStart(false)
let res: T | CaughtException
@@ -261,7 +305,7 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva
}
}
allowStateChangesEnd(prev)
- this.isComputing_ = false
+ this.isComputing = false
return res
}
diff --git a/packages/mobx/src/core/derivation.ts b/packages/mobx/src/core/derivation.ts
index 70ee16959d..b1c2d25048 100644
--- a/packages/mobx/src/core/derivation.ts
+++ b/packages/mobx/src/core/derivation.ts
@@ -166,10 +166,13 @@ export function checkIfStateReadsAreAllowed(observable: IObservable) {
*/
export function trackDerivedFunction(derivation: IDerivation, f: () => T, context: any) {
const prevAllowStateReads = allowStateReadsStart(true)
- // pre allocate array allocation + room for variation in deps
- // array will be trimmed by bindDependencies
changeDependenciesStateTo0(derivation)
- derivation.newObserving_ = new Array(derivation.observing_.length + 100)
+ // Preallocate array; will be trimmed by bindDependencies.
+ derivation.newObserving_ = new Array(
+ // Reserve constant space for initial dependencies, dynamic space otherwise.
+ // See https://github.com/mobxjs/mobx/pull/3833
+ derivation.runId_ === 0 ? 100 : derivation.observing_.length
+ )
derivation.unboundDepsCount_ = 0
derivation.runId_ = ++globalState.runId
const prevTracking = globalState.trackingDerivation
@@ -232,8 +235,8 @@ function bindDependencies(derivation: IDerivation) {
l = derivation.unboundDepsCount_
for (let i = 0; i < l; i++) {
const dep = observing[i]
- if (dep.diffValue_ === 0) {
- dep.diffValue_ = 1
+ if (dep.diffValue === 0) {
+ dep.diffValue = 1
if (i0 !== i) {
observing[i0] = dep
}
@@ -256,10 +259,10 @@ function bindDependencies(derivation: IDerivation) {
l = prevObserving.length
while (l--) {
const dep = prevObserving[l]
- if (dep.diffValue_ === 0) {
+ if (dep.diffValue === 0) {
removeObserver(dep, derivation)
}
- dep.diffValue_ = 0
+ dep.diffValue = 0
}
// Go through all new observables and check diffValue: (now it should be unique)
@@ -267,8 +270,8 @@ function bindDependencies(derivation: IDerivation) {
// 1: it wasn't observed, let's observe it. set back to 0
while (i0--) {
const dep = observing[i0]
- if (dep.diffValue_ === 1) {
- dep.diffValue_ = 0
+ if (dep.diffValue === 1) {
+ dep.diffValue = 0
addObserver(dep, derivation)
}
}
diff --git a/packages/mobx/src/core/observable.ts b/packages/mobx/src/core/observable.ts
index af6741e043..5ddbc74c86 100644
--- a/packages/mobx/src/core/observable.ts
+++ b/packages/mobx/src/core/observable.ts
@@ -17,17 +17,17 @@ export interface IDepTreeNode {
}
export interface IObservable extends IDepTreeNode {
- diffValue_: number
+ diffValue: number
/**
* Id of the derivation *run* that last accessed this observable.
* If this id equals the *run* id of the current derivation,
* the dependency is already established
*/
lastAccessedBy_: number
- isBeingObserved_: boolean
+ isBeingObserved: boolean
lowestObserverState_: IDerivationState_ // Used to avoid redundant propagations
- isPendingUnobservation_: boolean // Used to push itself to global.pendingUnobservations at most once per batch.
+ isPendingUnobservation: boolean // Used to push itself to global.pendingUnobservations at most once per batch.
observers_: Set
@@ -91,9 +91,9 @@ export function removeObserver(observable: IObservable, node: IDerivation) {
}
export function queueForUnobservation(observable: IObservable) {
- if (observable.isPendingUnobservation_ === false) {
+ if (observable.isPendingUnobservation === false) {
// invariant(observable._observers.length === 0, "INTERNAL ERROR, should only queue for unobservation unobserved observables");
- observable.isPendingUnobservation_ = true
+ observable.isPendingUnobservation = true
globalState.pendingUnobservations.push(observable)
}
}
@@ -114,11 +114,11 @@ export function endBatch() {
const list = globalState.pendingUnobservations
for (let i = 0; i < list.length; i++) {
const observable = list[i]
- observable.isPendingUnobservation_ = false
+ observable.isPendingUnobservation = false
if (observable.observers_.size === 0) {
- if (observable.isBeingObserved_) {
+ if (observable.isBeingObserved) {
// if this observable had reactive observers, trigger the hooks
- observable.isBeingObserved_ = false
+ observable.isBeingObserved = false
observable.onBUO()
}
if (observable instanceof ComputedValue) {
@@ -146,12 +146,12 @@ export function reportObserved(observable: IObservable): boolean {
observable.lastAccessedBy_ = derivation.runId_
// Tried storing newObserving, or observing, or both as Set, but performance didn't come close...
derivation.newObserving_![derivation.unboundDepsCount_++] = observable
- if (!observable.isBeingObserved_ && globalState.trackingContext) {
- observable.isBeingObserved_ = true
+ if (!observable.isBeingObserved && globalState.trackingContext) {
+ observable.isBeingObserved = true
observable.onBO()
}
}
- return true
+ return observable.isBeingObserved
} else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
queueForUnobservation(observable)
}
diff --git a/packages/mobx/src/core/reaction.ts b/packages/mobx/src/core/reaction.ts
index dadfdcb13b..d0c85b2d21 100644
--- a/packages/mobx/src/core/reaction.ts
+++ b/packages/mobx/src/core/reaction.ts
@@ -18,9 +18,12 @@ import {
spyReportStart,
startBatch,
trace,
- trackDerivedFunction
+ trackDerivedFunction,
+ GenericAbortSignal
} from "../internal"
+import { getFlag, setFlag } from "../utils/utils"
+
/**
* Reactions are a special kind of derivations. Several things distinguishes them from normal reactive computations
*
@@ -47,20 +50,23 @@ export interface IReactionPublic {
export interface IReactionDisposer {
(): void
- $mobx: Reaction
+ [$mobx]: Reaction
}
export class Reaction implements IDerivation, IReactionPublic {
observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes
newObserving_: IObservable[] = []
dependenciesState_ = IDerivationState_.NOT_TRACKING_
- diffValue_ = 0
runId_ = 0
unboundDepsCount_ = 0
- isDisposed_ = false
- isScheduled_ = false
- isTrackPending_ = false
- isRunning_ = false
+
+ private static readonly isDisposedMask_ = 0b00001
+ private static readonly isScheduledMask_ = 0b00010
+ private static readonly isTrackPendingMask_ = 0b00100
+ private static readonly isRunningMask_ = 0b01000
+ private static readonly diffValueMask_ = 0b10000
+ private flags_ = 0b00000
+
isTracing_: TraceMode = TraceMode.NONE
constructor(
@@ -70,37 +76,68 @@ export class Reaction implements IDerivation, IReactionPublic {
public requiresObservable_?
) {}
+ get isDisposed() {
+ return getFlag(this.flags_, Reaction.isDisposedMask_)
+ }
+ set isDisposed(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, Reaction.isDisposedMask_, newValue)
+ }
+
+ get isScheduled() {
+ return getFlag(this.flags_, Reaction.isScheduledMask_)
+ }
+ set isScheduled(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, Reaction.isScheduledMask_, newValue)
+ }
+
+ get isTrackPending() {
+ return getFlag(this.flags_, Reaction.isTrackPendingMask_)
+ }
+ set isTrackPending(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, Reaction.isTrackPendingMask_, newValue)
+ }
+
+ get isRunning() {
+ return getFlag(this.flags_, Reaction.isRunningMask_)
+ }
+ set isRunning(newValue: boolean) {
+ this.flags_ = setFlag(this.flags_, Reaction.isRunningMask_, newValue)
+ }
+
+ get diffValue(): 0 | 1 {
+ return getFlag(this.flags_, Reaction.diffValueMask_) ? 1 : 0
+ }
+ set diffValue(newValue: 0 | 1) {
+ this.flags_ = setFlag(this.flags_, Reaction.diffValueMask_, newValue === 1 ? true : false)
+ }
+
onBecomeStale_() {
this.schedule_()
}
schedule_() {
- if (!this.isScheduled_) {
- this.isScheduled_ = true
+ if (!this.isScheduled) {
+ this.isScheduled = true
globalState.pendingReactions.push(this)
runReactions()
}
}
- isScheduled() {
- return this.isScheduled_
- }
-
/**
* internal, use schedule() if you intend to kick off a reaction
*/
runReaction_() {
- if (!this.isDisposed_) {
+ if (!this.isDisposed) {
startBatch()
- this.isScheduled_ = false
+ this.isScheduled = false
const prev = globalState.trackingContext
globalState.trackingContext = this
if (shouldCompute(this)) {
- this.isTrackPending_ = true
+ this.isTrackPending = true
try {
this.onInvalidate_()
- if (__DEV__ && this.isTrackPending_ && isSpyEnabled()) {
+ if (__DEV__ && this.isTrackPending && isSpyEnabled()) {
// onInvalidate didn't trigger track right away..
spyReport({
name: this.name_,
@@ -117,7 +154,7 @@ export class Reaction implements IDerivation, IReactionPublic {
}
track(fn: () => void) {
- if (this.isDisposed_) {
+ if (this.isDisposed) {
return
// console.warn("Reaction already disposed") // Note: Not a warning / error in mobx 4 either
}
@@ -131,14 +168,14 @@ export class Reaction implements IDerivation, IReactionPublic {
type: "reaction"
})
}
- this.isRunning_ = true
+ this.isRunning = true
const prevReaction = globalState.trackingContext // reactions could create reactions...
globalState.trackingContext = this
const result = trackDerivedFunction(this, fn, undefined)
globalState.trackingContext = prevReaction
- this.isRunning_ = false
- this.isTrackPending_ = false
- if (this.isDisposed_) {
+ this.isRunning = false
+ this.isTrackPending = false
+ if (this.isDisposed) {
// disposed during last run. Clean up everything that was bound after the dispose call.
clearObserving(this)
}
@@ -184,9 +221,9 @@ export class Reaction implements IDerivation, IReactionPublic {
}
dispose() {
- if (!this.isDisposed_) {
- this.isDisposed_ = true
- if (!this.isRunning_) {
+ if (!this.isDisposed) {
+ this.isDisposed = true
+ if (!this.isRunning) {
// if disposed while running, clean up later. Maybe not optimal, but rare case
startBatch()
clearObserving(this)
@@ -195,10 +232,19 @@ export class Reaction implements IDerivation, IReactionPublic {
}
}
- getDisposer_(): IReactionDisposer {
- const r = this.dispose.bind(this) as IReactionDisposer
- r[$mobx] = this
- return r
+ getDisposer_(abortSignal?: GenericAbortSignal): IReactionDisposer {
+ const dispose = (() => {
+ this.dispose()
+ abortSignal?.removeEventListener?.("abort", dispose)
+ }) as IReactionDisposer
+ abortSignal?.addEventListener?.("abort", dispose)
+ dispose[$mobx] = this
+
+ if ("dispose" in Symbol && typeof Symbol.dispose === "symbol") {
+ dispose[Symbol.dispose] = dispose
+ }
+
+ return dispose
}
toString() {
diff --git a/packages/mobx/src/internal.ts b/packages/mobx/src/internal.ts
index c476084fda..1317b234c3 100644
--- a/packages/mobx/src/internal.ts
+++ b/packages/mobx/src/internal.ts
@@ -18,6 +18,7 @@ export * from "./types/flowannotation"
export * from "./types/computedannotation"
export * from "./types/observableannotation"
export * from "./types/autoannotation"
+export * from "./types/generic-abort-signal"
export * from "./api/observable"
export * from "./api/computed"
export * from "./core/action"
diff --git a/packages/mobx/src/mobx.ts b/packages/mobx/src/mobx.ts
index 315ee4a632..cb366b52a9 100644
--- a/packages/mobx/src/mobx.ts
+++ b/packages/mobx/src/mobx.ts
@@ -2,7 +2,7 @@
* (c) Michel Weststrate 2015 - 2020
* MIT Licensed
*
- * Welcome to the mobx sources! To get an global overview of how MobX internally works,
+ * Welcome to the mobx sources! To get a global overview of how MobX internally works,
* this is a good place to start:
* https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.xvbh6qd74
*
@@ -108,6 +108,7 @@ export {
flow,
isFlow,
flowResult,
+ CancellablePromise,
FlowCancellationError,
isFlowCancellationError,
toJS,
diff --git a/packages/mobx/src/types/actionannotation.ts b/packages/mobx/src/types/actionannotation.ts
index 7385c108b8..ab9d354bb3 100644
--- a/packages/mobx/src/types/actionannotation.ts
+++ b/packages/mobx/src/types/actionannotation.ts
@@ -7,7 +7,8 @@ import {
isFunction,
Annotation,
globalState,
- MakeResult
+ MakeResult,
+ assert20223DecoratorType
} from "../internal"
export function createActionAnnotation(name: string, options?: object): Annotation {
@@ -15,11 +16,13 @@ export function createActionAnnotation(name: string, options?: object): Annotati
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -49,6 +52,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -58,6 +62,53 @@ function extend_(
return adm.defineProperty_(key, actionDescriptor, proxyTrap)
}
+function decorate_20223_(this: Annotation, mthd, context: DecoratorContext) {
+ if (__DEV__) {
+ assert20223DecoratorType(context, ["method", "field"])
+ }
+ const { kind, name, addInitializer } = context
+ const ann = this
+
+ const _createAction = m =>
+ createAction(ann.options_?.name ?? name!.toString(), m, ann.options_?.autoAction ?? false)
+
+ if (kind == "field") {
+ return function (initMthd) {
+ let mthd = initMthd
+ if (!isAction(mthd)) {
+ mthd = _createAction(mthd)
+ }
+ if (ann.options_?.bound) {
+ mthd = mthd.bind(this)
+ mthd.isMobxAction = true
+ }
+ return mthd
+ }
+ }
+
+ if (kind == "method") {
+ if (!isAction(mthd)) {
+ mthd = _createAction(mthd)
+ }
+
+ if (this.options_?.bound) {
+ addInitializer(function () {
+ const self = this as any
+ const bound = self[name].bind(self)
+ bound.isMobxAction = true
+ self[name] = bound
+ })
+ }
+
+ return mthd
+ }
+
+ die(
+ `Cannot apply '${ann.annotationType_}' to '${String(name)}' (kind: ${kind}):` +
+ `\n'${ann.annotationType_}' can only be used on properties with a function value.`
+ )
+}
+
function assertActionDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/autoannotation.ts b/packages/mobx/src/types/autoannotation.ts
index 1be4e26363..902e741213 100644
--- a/packages/mobx/src/types/autoannotation.ts
+++ b/packages/mobx/src/types/autoannotation.ts
@@ -9,7 +9,9 @@ import {
computed,
autoAction,
isGenerator,
- MakeResult
+ MakeResult,
+ die,
+ isAction
} from "../internal"
const AUTO = "true"
@@ -21,7 +23,8 @@ export function createAutoAnnotation(options?: object): Annotation {
annotationType_: AUTO,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
@@ -38,7 +41,9 @@ function make_(
// lone setter -> action setter
if (descriptor.set) {
// TODO make action applicable to setter and delegate to action.make_
- const set = createAction(key.toString(), descriptor.set) as (v: any) => void
+ const set = isAction(descriptor.set)
+ ? descriptor.set // See #4553
+ : (createAction(key.toString(), descriptor.set) as (v: any) => void)
// own
if (source === adm.target_) {
return adm.defineProperty_(key, {
@@ -105,3 +110,7 @@ function extend_(
let observableAnnotation = this.options_?.deep === false ? observable.ref : observable
return observableAnnotation.extend_(adm, key, descriptor, proxyTrap)
}
+
+function decorate_20223_(this: Annotation, desc, context: ClassGetterDecoratorContext) {
+ die(`'${this.annotationType_}' cannot be used as a decorator`)
+}
diff --git a/packages/mobx/src/types/computedannotation.ts b/packages/mobx/src/types/computedannotation.ts
index 4caa11f6eb..dd398741f4 100644
--- a/packages/mobx/src/types/computedannotation.ts
+++ b/packages/mobx/src/types/computedannotation.ts
@@ -1,15 +1,26 @@
-import { ObservableObjectAdministration, die, Annotation, MakeResult } from "../internal"
+import {
+ ObservableObjectAdministration,
+ die,
+ Annotation,
+ MakeResult,
+ assert20223DecoratorType,
+ $mobx,
+ asObservableObject,
+ ComputedValue
+} from "../internal"
export function createComputedAnnotation(name: string, options?: object): Annotation {
return {
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor
@@ -18,6 +29,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -35,6 +47,31 @@ function extend_(
)
}
+function decorate_20223_(this: Annotation, get, context: ClassGetterDecoratorContext) {
+ if (__DEV__) {
+ assert20223DecoratorType(context, ["getter"])
+ }
+ const ann = this
+ const { name: key, addInitializer } = context
+
+ addInitializer(function () {
+ const adm: ObservableObjectAdministration = asObservableObject(this)[$mobx]
+ const options = {
+ ...ann.options_,
+ get,
+ context: this
+ }
+ options.name ||= __DEV__
+ ? `${adm.name_}.${key.toString()}`
+ : `ObservableObject.${key.toString()}`
+ adm.values_.set(key, new ComputedValue(options))
+ })
+
+ return function () {
+ return this[$mobx].getObservablePropValue_(key)
+ }
+}
+
function assertComputedDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/decorator_fills.ts b/packages/mobx/src/types/decorator_fills.ts
new file mode 100644
index 0000000000..084f023376
--- /dev/null
+++ b/packages/mobx/src/types/decorator_fills.ts
@@ -0,0 +1,33 @@
+// Hopefully these will be main-lined into Typescipt, but at the moment TS only declares the Contexts
+
+export type ClassAccessorDecorator = (
+ value: ClassAccessorDecoratorTarget,
+ context: ClassAccessorDecoratorContext
+) => ClassAccessorDecoratorResult | void
+
+export type ClassGetterDecorator = (
+ value: (this: This) => Value,
+ context: ClassGetterDecoratorContext
+) => ((this: This) => Value) | void
+
+export type ClassSetterDecorator = (
+ value: (this: This, value: Value) => void,
+ context: ClassSetterDecoratorContext
+) => ((this: This, value: Value) => void) | void
+
+export type ClassMethodDecorator any = any> = (
+ value: Value,
+ context: ClassMethodDecoratorContext
+) => Value | void
+
+export type ClassFieldDecorator any = any> = (
+ value: Value,
+ context: ClassFieldDecoratorContext
+) => Value | void
+
+export type Decorator =
+ | ClassAccessorDecorator
+ | ClassGetterDecorator
+ | ClassSetterDecorator
+ | ClassMethodDecorator
+ | ClassFieldDecorator
diff --git a/packages/mobx/src/types/flowannotation.ts b/packages/mobx/src/types/flowannotation.ts
index 42d6f3e109..530b95942c 100644
--- a/packages/mobx/src/types/flowannotation.ts
+++ b/packages/mobx/src/types/flowannotation.ts
@@ -8,7 +8,8 @@ import {
isFunction,
globalState,
MakeResult,
- hasProp
+ hasProp,
+ assert20223DecoratorType
} from "../internal"
export function createFlowAnnotation(name: string, options?: object): Annotation {
@@ -16,11 +17,13 @@ export function createFlowAnnotation(name: string, options?: object): Annotation
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -50,6 +53,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -59,6 +63,28 @@ function extend_(
return adm.defineProperty_(key, flowDescriptor, proxyTrap)
}
+function decorate_20223_(this: Annotation, mthd, context: ClassMethodDecoratorContext) {
+ if (__DEV__) {
+ assert20223DecoratorType(context, ["method"])
+ }
+ const { name, addInitializer } = context
+
+ if (!isFlow(mthd)) {
+ mthd = flow(mthd)
+ }
+
+ if (this.options_?.bound) {
+ addInitializer(function () {
+ const self = this as any
+ const bound = self[name].bind(self)
+ bound.isMobXFlow = true
+ self[name] = bound
+ })
+ }
+
+ return mthd
+}
+
function assertFlowDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/generic-abort-signal.ts b/packages/mobx/src/types/generic-abort-signal.ts
new file mode 100644
index 0000000000..3620496ff2
--- /dev/null
+++ b/packages/mobx/src/types/generic-abort-signal.ts
@@ -0,0 +1,7 @@
+// https://github.com/mobxjs/mobx/issues/3582
+export interface GenericAbortSignal {
+ readonly aborted: boolean
+ onabort?: ((...args: any) => any) | null
+ addEventListener?: (...args: any) => any
+ removeEventListener?: (...args: any) => any
+}
diff --git a/packages/mobx/src/types/legacyobservablearray.ts b/packages/mobx/src/types/legacyobservablearray.ts
index 253f0c1777..5ca88d07a1 100644
--- a/packages/mobx/src/types/legacyobservablearray.ts
+++ b/packages/mobx/src/types/legacyobservablearray.ts
@@ -1,8 +1,6 @@
import {
getNextId,
addHiddenFinalProp,
- allowStateChangesStart,
- allowStateChangesEnd,
makeIterable,
addHiddenProp,
ObservableArrayAdministration,
@@ -11,9 +9,25 @@ import {
IEnhancer,
isObservableArray,
IObservableArray,
- defineProperty
+ defineProperty,
+ initObservable
} from "../internal"
+// Bug in safari 9.* (or iOS 9 safari mobile). See #364
+const ENTRY_0 = createArrayEntryDescriptor(0)
+
+const safariPrototypeSetterInheritanceBug = (() => {
+ let v = false
+ const p = {}
+ Object.defineProperty(p, "0", {
+ set: () => {
+ v = true
+ }
+ })
+ Object.create(p)["0"] = 1
+ return v === false
+})()
+
/**
* This array buffer contains two lists of properties, so that all arrays
* can recycle their property definitions, which significantly improves performance of creating
@@ -38,7 +52,7 @@ inherit(StubArray, Array.prototype)
// but it is unclear why the hack is need as MobX never changed the prototype
// anyway, so removed it in V6
-class LegacyObservableArray extends StubArray {
+export class LegacyObservableArray extends StubArray {
constructor(
initialValues: T[] | undefined,
enhancer: IEnhancer,
@@ -46,17 +60,22 @@ class LegacyObservableArray extends StubArray {
owned = false
) {
super()
+ initObservable(() => {
+ const adm = new ObservableArrayAdministration(name, enhancer, owned, true)
+ adm.proxy_ = this as any
+ addHiddenFinalProp(this, $mobx, adm)
+
+ if (initialValues && initialValues.length) {
+ // @ts-ignore
+ this.spliceWithArray(0, 0, initialValues)
+ }
- const adm = new ObservableArrayAdministration(name, enhancer, owned, true)
- adm.proxy_ = this as any
- addHiddenFinalProp(this, $mobx, adm)
-
- if (initialValues && initialValues.length) {
- const prev = allowStateChangesStart(true)
- // @ts-ignore
- this.spliceWithArray(0, 0, initialValues)
- allowStateChangesEnd(prev)
- }
+ if (safariPrototypeSetterInheritanceBug) {
+ // Seems that Safari won't use numeric prototype setter until any * numeric property is
+ // defined on the instance. After that it works fine, even if this property is deleted.
+ Object.defineProperty(this, "0", ENTRY_0)
+ }
+ })
}
concat(...arrays: T[][]): T[] {
diff --git a/packages/mobx/src/types/observableannotation.ts b/packages/mobx/src/types/observableannotation.ts
index 4e5175e431..69dee5775b 100644
--- a/packages/mobx/src/types/observableannotation.ts
+++ b/packages/mobx/src/types/observableannotation.ts
@@ -3,7 +3,11 @@ import {
deepEnhancer,
die,
Annotation,
- MakeResult
+ MakeResult,
+ assert20223DecoratorType,
+ ObservableValue,
+ asObservableObject,
+ $mobx
} from "../internal"
export function createObservableAnnotation(name: string, options?: object): Annotation {
@@ -11,11 +15,13 @@ export function createObservableAnnotation(name: string, options?: object): Anno
annotationType_: name,
options_: options,
make_,
- extend_
+ extend_,
+ decorate_20223_
}
}
function make_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor
@@ -24,6 +30,7 @@ function make_(
}
function extend_(
+ this: Annotation,
adm: ObservableObjectAdministration,
key: PropertyKey,
descriptor: PropertyDescriptor,
@@ -38,6 +45,72 @@ function extend_(
)
}
+function decorate_20223_(
+ this: Annotation,
+ desc,
+ context: ClassAccessorDecoratorContext | ClassFieldDecoratorContext
+) {
+ if (__DEV__) {
+ if (context.kind === "field") {
+ throw die(
+ `Please use \`@observable accessor ${String(
+ context.name
+ )}\` instead of \`@observable ${String(context.name)}\``
+ )
+ }
+ assert20223DecoratorType(context, ["accessor"])
+ }
+
+ const ann = this
+ const { kind, name } = context
+
+ // The laziness here is not ideal... It's a workaround to how 2022.3 Decorators are implemented:
+ // `addInitializer` callbacks are executed _before_ any accessors are defined (instead of the ideal-for-us right after each).
+ // This means that, if we were to do our stuff in an `addInitializer`, we'd attempt to read a private slot
+ // before it has been initialized. The runtime doesn't like that and throws a `Cannot read private member
+ // from an object whose class did not declare it` error.
+ // TODO: it seems that this will not be required anymore in the final version of the spec
+ // See TODO: link
+ const initializedObjects = new WeakSet()
+
+ function initializeObservable(target, value) {
+ const adm: ObservableObjectAdministration = asObservableObject(target)[$mobx]
+ const observable = new ObservableValue(
+ value,
+ ann.options_?.enhancer ?? deepEnhancer,
+ __DEV__ ? `${adm.name_}.${name.toString()}` : `ObservableObject.${name.toString()}`,
+ false
+ )
+ adm.values_.set(name, observable)
+ initializedObjects.add(target)
+ }
+
+ if (kind == "accessor") {
+ return {
+ get() {
+ if (!initializedObjects.has(this)) {
+ initializeObservable(this, desc.get.call(this))
+ }
+ return this[$mobx].getObservablePropValue_(name)
+ },
+ set(value) {
+ if (!initializedObjects.has(this)) {
+ initializeObservable(this, value)
+ }
+ return this[$mobx].setObservablePropValue_(name, value)
+ },
+ init(value) {
+ if (!initializedObjects.has(this)) {
+ initializeObservable(this, value)
+ }
+ return value
+ }
+ }
+ }
+
+ return
+}
+
function assertObservableDescriptor(
adm: ObservableObjectAdministration,
{ annotationType_ }: Annotation,
diff --git a/packages/mobx/src/types/observablearray.ts b/packages/mobx/src/types/observablearray.ts
index 6c720f2dd6..18f45a1f99 100644
--- a/packages/mobx/src/types/observablearray.ts
+++ b/packages/mobx/src/types/observablearray.ts
@@ -22,13 +22,12 @@ import {
registerListener,
spyReportEnd,
spyReportStart,
- allowStateChangesStart,
- allowStateChangesEnd,
assertProxies,
reserveArrayBuffer,
hasProp,
die,
- globalState
+ globalState,
+ initObservable
} from "../internal"
const SPLICE = "splice"
@@ -345,19 +344,24 @@ export class ObservableArrayAdministration
}
get_(index: number): any | undefined {
- if (index < this.values_.length) {
- this.atom_.reportObserved()
- return this.dehanceValue_(this.values_[index])
- }
- console.warn(
- __DEV__
- ? `[mobx] Out of bounds read: ${index}`
- : `[mobx.array] Attempt to read an array index (${index}) that is out of bounds (${this.values_.length}). Please check length first. Out of bound indices will not be tracked by MobX`
- )
+ if (this.legacyMode_ && index >= this.values_.length) {
+ console.warn(
+ __DEV__
+ ? `[mobx.array] Attempt to read an array index (${index}) that is out of bounds (${this.values_.length}). Please check length first. Out of bound indices will not be tracked by MobX`
+ : `[mobx] Out of bounds read: ${index}`
+ )
+ return undefined
+ }
+ this.atom_.reportObserved()
+ return this.dehanceValue_(this.values_[index])
}
set_(index: number, newValue: any) {
const values = this.values_
+ if (this.legacyMode_ && index > values.length) {
+ // out of bounds
+ die(17, index, values.length)
+ }
if (index < values.length) {
// update at index in range
checkIfStateModificationsAreAllowed(this.atom_)
@@ -380,12 +384,16 @@ export class ObservableArrayAdministration
values[index] = newValue
this.notifyArrayChildUpdate_(index, newValue, oldValue)
}
- } else if (index === values.length) {
- // add a new item
- this.spliceWithArray_(index, 0, [newValue])
} else {
- // out of bounds
- die(17, index, values.length)
+ // For out of bound index, we don't create an actual sparse array,
+ // but rather fill the holes with undefined (same as setArrayLength_).
+ // This could be considered a bug.
+ const newItems = new Array(index + 1 - values.length)
+ for (let i = 0; i < newItems.length - 1; i++) {
+ newItems[i] = undefined
+ } // No Array.fill everywhere...
+ newItems[newItems.length - 1] = newValue
+ this.spliceWithArray_(values.length, 0, newItems)
}
}
}
@@ -397,16 +405,16 @@ export function createObservableArray(
owned = false
): IObservableArray {
assertProxies()
- const adm = new ObservableArrayAdministration(name, enhancer, owned, false)
- addHiddenFinalProp(adm.values_, $mobx, adm)
- const proxy = new Proxy(adm.values_, arrayTraps) as any
- adm.proxy_ = proxy
- if (initialValues && initialValues.length) {
- const prev = allowStateChangesStart(true)
- adm.spliceWithArray_(0, 0, initialValues)
- allowStateChangesEnd(prev)
- }
- return proxy
+ return initObservable(() => {
+ const adm = new ObservableArrayAdministration(name, enhancer, owned, false)
+ addHiddenFinalProp(adm.values_, $mobx, adm)
+ const proxy = new Proxy(adm.values_, arrayTraps) as any
+ adm.proxy_ = proxy
+ if (initialValues && initialValues.length) {
+ adm.spliceWithArray_(0, 0, initialValues)
+ }
+ return proxy
+ })
}
// eslint-disable-next-line
@@ -510,6 +518,7 @@ export var arrayExtensions = {
* Without this, everything works as well, but this works
* faster as everything works on unproxied values
*/
+addArrayExtension("at", simpleFunc)
addArrayExtension("concat", simpleFunc)
addArrayExtension("flat", simpleFunc)
addArrayExtension("includes", simpleFunc)
@@ -519,15 +528,21 @@ addArrayExtension("lastIndexOf", simpleFunc)
addArrayExtension("slice", simpleFunc)
addArrayExtension("toString", simpleFunc)
addArrayExtension("toLocaleString", simpleFunc)
+addArrayExtension("toSorted", simpleFunc)
+addArrayExtension("toSpliced", simpleFunc)
+addArrayExtension("with", simpleFunc)
// map
addArrayExtension("every", mapLikeFunc)
addArrayExtension("filter", mapLikeFunc)
addArrayExtension("find", mapLikeFunc)
addArrayExtension("findIndex", mapLikeFunc)
+addArrayExtension("findLast", mapLikeFunc)
+addArrayExtension("findLastIndex", mapLikeFunc)
addArrayExtension("flatMap", mapLikeFunc)
addArrayExtension("forEach", mapLikeFunc)
addArrayExtension("map", mapLikeFunc)
addArrayExtension("some", mapLikeFunc)
+addArrayExtension("toReversed", mapLikeFunc)
// reduce
addArrayExtension("reduce", reduceLikeFunc)
addArrayExtension("reduceRight", reduceLikeFunc)
@@ -548,7 +563,7 @@ function simpleFunc(funcName) {
}
}
-// Make sure callbacks recieve correct array arg #2326
+// Make sure callbacks receive correct array arg #2326
function mapLikeFunc(funcName) {
return function (callback, thisArg) {
const adm: ObservableArrayAdministration = this[$mobx]
@@ -560,7 +575,7 @@ function mapLikeFunc(funcName) {
}
}
-// Make sure callbacks recieve correct array arg #2326
+// Make sure callbacks receive correct array arg #2326
function reduceLikeFunc(funcName) {
return function () {
const adm: ObservableArrayAdministration = this[$mobx]
diff --git a/packages/mobx/src/types/observablemap.ts b/packages/mobx/src/types/observablemap.ts
index 9b30433e00..014b65507c 100644
--- a/packages/mobx/src/types/observablemap.ts
+++ b/packages/mobx/src/types/observablemap.ts
@@ -9,6 +9,7 @@ import {
checkIfStateModificationsAreAllowed,
createAtom,
createInstanceofPredicate,
+ makeIterable,
deepEnhancer,
getNextId,
getPlainObjectKeys,
@@ -16,9 +17,9 @@ import {
hasListeners,
interceptChange,
isES6Map,
+ isPlainES6Map,
isPlainObject,
isSpyEnabled,
- makeIterable,
notifyListeners,
referenceEnhancer,
registerInterceptor,
@@ -35,7 +36,7 @@ import {
UPDATE,
IAtom,
PureSpyEvent,
- allowStateChanges
+ initObservable
} from "../internal"
export interface IKeyValueMap {
@@ -43,7 +44,9 @@ export interface IKeyValueMap {
}
export type IMapEntry = [K, V]
+export type IReadonlyMapEntry = readonly [K, V]
export type IMapEntries = IMapEntry[]
+export type IReadonlyMapEntries = readonly IReadonlyMapEntry[]
export type IMapDidChange = { observableKind: "map"; debugObjectName: string } & (
| {
@@ -81,6 +84,7 @@ export const DELETE = "delete"
export type IObservableMapInitialValues =
| IMapEntries
+ | IReadonlyMapEntries
| IKeyValueMap
| Map
@@ -90,9 +94,9 @@ export class ObservableMap
implements Map, IInterceptable>, IListenable
{
[$mobx] = ObservableMapMarker
- data_: Map>
- hasMap_: Map> // hasMap, not hashMap >-).
- keysAtom_: IAtom
+ data_!: Map>
+ hasMap_!: Map> // hasMap, not hashMap >-).
+ keysAtom_!: IAtom
interceptors_
changeListeners_
dehancer: any
@@ -105,11 +109,13 @@ export class ObservableMap