Repository: mobxjs/mobx-state-tree Branch: master Commit: 24037f14c6b8 Files: 206 Total size: 1.4 MB Directory structure: gitextract_v8l79haa/ ├── .circleci/ │ └── config.yml ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug-report.md │ ├── lock.yml │ ├── pull_request_template.md │ └── stale.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __tests__/ │ ├── core/ │ │ ├── 1525.test.ts │ │ ├── 1664.test.ts │ │ ├── 2230.test.ts │ │ ├── __snapshots__/ │ │ │ ├── async.test.ts.snap │ │ │ ├── custom-type.test.ts.snap │ │ │ └── reference-custom.test.ts.snap │ │ ├── action.test.ts │ │ ├── actionTrackingMiddleware2.test.ts │ │ ├── api.test.ts │ │ ├── array.test.ts │ │ ├── async.test.ts │ │ ├── bigint.test.ts │ │ ├── boolean.test.ts │ │ ├── boxes-store.test.ts │ │ ├── circular1.test.ts │ │ ├── circular2.test.ts │ │ ├── custom-type.test.ts │ │ ├── date.test.ts │ │ ├── deprecated.test.ts │ │ ├── enum.test.ts │ │ ├── env.test.ts │ │ ├── frozen.test.ts │ │ ├── hooks.test.ts │ │ ├── identifier.test.ts │ │ ├── jsonpatch.test.ts │ │ ├── late.test.ts │ │ ├── lazy.test.ts │ │ ├── literal.test.ts │ │ ├── map.test.ts │ │ ├── model.test.ts │ │ ├── name.test.ts │ │ ├── node.test.ts │ │ ├── number.test.ts │ │ ├── object-node.test.ts │ │ ├── object.test.ts │ │ ├── optimizations.test.ts │ │ ├── optional-extension.test.ts │ │ ├── optional.test.ts │ │ ├── parent-properties.test.ts │ │ ├── pointer.test.ts │ │ ├── primitives.test.ts │ │ ├── protect.test.ts │ │ ├── recordPatches.test.ts │ │ ├── reference-custom.test.ts │ │ ├── reference-onInvalidated.test.ts │ │ ├── reference.test.ts │ │ ├── refinement.test.ts │ │ ├── reflection.test.ts │ │ ├── snapshotProcessor.test.ts │ │ ├── string.test.ts │ │ ├── this.test.ts │ │ ├── type-system.test.ts │ │ ├── union.test.ts │ │ └── volatile.test.ts │ ├── perf/ │ │ ├── fixture-data.skip.ts │ │ ├── fixture-models.skip.ts │ │ ├── fixtures/ │ │ │ ├── fixture-data.ts │ │ │ └── fixture-models.ts │ │ ├── perf.skip.ts │ │ ├── report.ts │ │ ├── scenarios.ts │ │ └── timer.ts │ ├── setup.ts │ ├── tsconfig.json │ └── utils.test.ts ├── bun.lockb ├── bunfig.toml ├── changelog.md ├── docker-compose.yml ├── docs/ │ ├── .gitattributes │ ├── API/ │ │ ├── index.md │ │ └── interfaces/ │ │ ├── customtypeoptions.md │ │ ├── functionwithflag.md │ │ ├── iactioncontext.md │ │ ├── iactionrecorder.md │ │ ├── iactiontrackingmiddleware2call.md │ │ ├── iactiontrackingmiddleware2hooks.md │ │ ├── iactiontrackingmiddlewarehooks.md │ │ ├── ianycomplextype.md │ │ ├── ianymodeltype.md │ │ ├── ianytype.md │ │ ├── ihooks.md │ │ ├── ijsonpatch.md │ │ ├── imiddlewareevent.md │ │ ├── imodelreflectiondata.md │ │ ├── imodelreflectionpropertiesdata.md │ │ ├── imodeltype.md │ │ ├── ipatchrecorder.md │ │ ├── ireversiblejsonpatch.md │ │ ├── iserializedactioncall.md │ │ ├── isimpletype.md │ │ ├── isnapshotprocessor.md │ │ ├── isnapshotprocessors.md │ │ ├── itype.md │ │ ├── ivalidationcontextentry.md │ │ ├── ivalidationerror.md │ │ ├── referenceoptionsgetset.md │ │ ├── referenceoptionsoninvalidated.md │ │ └── unionoptions.md │ ├── API_header.md │ ├── compare/ │ │ └── context-reducer-vs-mobx-state-tree.md │ ├── concepts/ │ │ ├── actions.md │ │ ├── async-actions.md │ │ ├── dependency-injection.md │ │ ├── listeners.md │ │ ├── middleware.md │ │ ├── patches.md │ │ ├── react.md │ │ ├── reconciliation.md │ │ ├── references.md │ │ ├── snapshots.md │ │ ├── trees.md │ │ ├── views.md │ │ └── volatiles.md │ ├── intro/ │ │ ├── examples.md │ │ ├── getting-started.md │ │ ├── installation.md │ │ ├── philosophy.md │ │ └── welcome.md │ ├── overview/ │ │ ├── hooks.md │ │ ├── types.md │ │ └── utilties.md │ ├── recipes/ │ │ ├── auto-generated-property-setter-actions.md │ │ ├── mst-query.md │ │ └── pre-built-form-types-with-mst-form-type.md │ └── tips/ │ ├── circular-deps.md │ ├── faq.md │ ├── inheritance.md │ ├── more-tips.md │ ├── resources.md │ ├── snapshots-as-values.md │ └── typescript.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── scripts/ │ ├── fix-docs-source-links.js │ ├── generate-compose-type.js │ └── generate-shared.js ├── src/ │ ├── core/ │ │ ├── action.ts │ │ ├── actionContext.ts │ │ ├── flow.ts │ │ ├── json-patch.ts │ │ ├── mst-operations.ts │ │ ├── node/ │ │ │ ├── BaseNode.ts │ │ │ ├── Hook.ts │ │ │ ├── create-node.ts │ │ │ ├── identifier-cache.ts │ │ │ ├── livelinessChecking.ts │ │ │ ├── node-utils.ts │ │ │ ├── object-node.ts │ │ │ └── scalar-node.ts │ │ ├── process.ts │ │ └── type/ │ │ ├── type-checker.ts │ │ └── type.ts │ ├── index.ts │ ├── internal.ts │ ├── middlewares/ │ │ ├── create-action-tracking-middleware.ts │ │ ├── createActionTrackingMiddleware2.ts │ │ └── on-action.ts │ ├── types/ │ │ ├── complex-types/ │ │ │ ├── array.ts │ │ │ ├── map.ts │ │ │ └── model.ts │ │ ├── index.ts │ │ ├── primitives.ts │ │ └── utility-types/ │ │ ├── custom.ts │ │ ├── enumeration.ts │ │ ├── frozen.ts │ │ ├── identifier.ts │ │ ├── late.ts │ │ ├── lazy.ts │ │ ├── literal.ts │ │ ├── maybe.ts │ │ ├── optional.ts │ │ ├── reference.ts │ │ ├── refinement.ts │ │ ├── snapshotProcessor.ts │ │ └── union.ts │ └── utils.ts ├── test-results/ │ └── .gitkeep ├── tsconfig.json ├── tslint.json ├── typedocconfig.js └── website/ ├── bun.lockb ├── core/ │ └── Footer.js ├── i18n/ │ └── en.json ├── package.json ├── sidebars.json ├── siteConfig.js └── static/ ├── css/ │ └── custom.css └── index.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2.1 executors: my-executor: docker: - image: cimg/node:14.18.1 environment: CI: true orbs: node: circleci/node@4.7.0 jobs: # mobx-state-tree build build: executor: my-executor steps: - checkout - run: name: Install the latest version of bun command: curl -fsSL https://bun.sh/install | bash - run: name: Link bun command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/ - run: name: Install dependencies command: bun install - run: name: Build MST command: bun run build - persist_to_workspace: root: . paths: - ./* # Add new prettier check job check-prettier: executor: my-executor steps: - attach_workspace: at: . - run: name: Install the latest version of bun command: curl -fsSL https://bun.sh/install | bash - run: name: Link bun command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/ - run: name: Check code formatting command: bun run prettier:check # mobx-state-tree tests test-mst-dev: executor: my-executor steps: - attach_workspace: at: . - run: name: Install the latest version of bun command: curl -fsSL https://bun.sh/install | bash - run: name: Link bun command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/ - run: bun test:all test-mst-prod: executor: my-executor steps: - attach_workspace: at: . - run: name: Install the latest version of bun command: curl -fsSL https://bun.sh/install | bash - run: name: Link bun command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/ - run: bun test:prod test-size: executor: my-executor steps: - attach_workspace: at: . - run: name: Install the latest version of bun command: curl -fsSL https://bun.sh/install | bash - run: name: Link bun command: sudo ln -s ~/.bun/bin/bun /usr/local/bin/ - run: bun run size workflows: version: 2 build-and-test: jobs: - build # Add prettier check to workflow - check-prettier: requires: - build - test-mst-dev: requires: - build - test-mst-prod: requires: - build # Temporarily disabled while we work to implement Bun testing # - test-size: # requires: # - build ================================================ FILE: .dockerignore ================================================ */node_modules *.log ================================================ FILE: .gitattributes ================================================ * text=auto *.ts text eol=lf *.json text eol=lf *.md text eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug report about: I think something is not working as it should --- **_Bug report_** - [ ] I've checked documentation and searched for existing issues [and discussions](https://github.com/mobxjs/mobx-state-tree/discussions) - [ ] I've made sure my project is based on the latest MST version - [ ] Fork [this](https://codesandbox.io/p/sandbox/mobx-state-tree-todolist-forked-pj732k) code sandbox or another minimal reproduction. **Sandbox link or minimal reproduction code** **Describe the expected behavior** **Describe the observed behavior** ================================================ FILE: .github/lock.yml ================================================ # Configuration for Lock Threads - https://github.com/dessant/lock-threads # Number of days of inactivity before a closed issue or pull request is locked daysUntilLock: 60 # Skip issues and pull requests created before a given timestamp. Timestamp must # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable skipCreatedBefore: 2019-01-01 # Issues and pull requests with these labels will be ignored. Set to `[]` to disable exemptLabels: [] # Label to add before locking, such as `outdated`. Set to `false` to disable lockLabel: false # Comment to post before locking. Set to `false` to disable lockComment: > This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or questions. # Assign `resolved` as the reason for locking. Set to `false` to disable setLockReason: true # Limit to only `issues` or `pulls` only: issues ================================================ FILE: .github/pull_request_template.md ================================================ ## What does this PR do and why? ## Steps to validate locally ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 10 # Number of days of inactivity before a stale issue is closed daysUntilClose: 4 # Issues with these labels will never be considered stale exemptLabels: - brainstorming/wild idea - breaking change - bug - docs or examples - enhancement - has PR - help/PR welcome - require('@mweststrate') - never-stale # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity in the last 10 days. It will be closed in 4 days if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false # Limit to only `issues` or `pulls` only: issues # Comment to post when removing the stale label. unmarkComment: > This issue has been automatically unmarked as stale. Please disregard previous warnings. ================================================ FILE: .gitignore ================================================ node_modules *.log lib dist coverage .nyc_output .idea package-lock.json .prettierignore .vscode .editorconfig /test-results/**/*.xml /website/build .DS_Store junit.xml ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" bun run lint-staged ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to MobX-State-Tree Welcome to MobX-State-Tree! We're stoked that you want to contribute to our open-source project. Our community is essential in maintaining and improving the stability, test coverage, and documentation of MST. We really appreciate your time and interest in pitching in. Overall, we want to build useful software and have fun doing it - we hope you'll be able to join us! ## Table of Contents 1. [Getting Started](#getting-started) 2. [Contributing Guidelines](#contributing-guidelines) 3. [Reporting Bugs](#reporting-bugs) 4. [Code of Conduct](#code-of-conduct) ## Getting Started Before you start contributing, please make sure you have: - [Bun](https://bun.sh/) installed on your local machine. - A [GitHub](https://github.com/) account, as you'll need it to create issues and submit pull requests. Most of the documentation and community assumes some amount of familiarity with: 1. JavaScript 2. TypeScript 3. git 4. Using the command line 5. Experience with state management libraries (typically on the frontend, although there are plenty of applications using MST in other contexts). If you don't feel comfortable with these concepts, we'd be happy to help you get started, but you may want to consider brushing up on them before digging into the codebase. Reach out in the [discussions section of our GitHub repository](https://github.com/mobxjs/mobx-state-tree/discussions) if you'd like pointers about where to start. ## Contributing Guidelines ### Prioritizing Stability Over New Features The existing API for MobX-State-Tree is [already quite extensive](https://mobx-state-tree.js.org/intro/welcome). As such, issues and PRs about new features may take lower priority than bug fixes, improving existing features, and performance/TypeScript improvements. If you're looking to augment MST with new functionality, we encourage you to consider building your own third-party library around our project, or perhaps [contributing to the mst-middlewares package](https://github.com/coolsoftwaretyler/mst-middlewares). We'd be happy to help faciliate that work, especially if it keeps our API from expanding much further. ### Tests To maintain our library's stability, we are always striving to improve test coverage, even where more tests might be redundant. Every PR is _required to add at least one test that directly exercises your code change_, even if that test may be duplicative of existing tests. If you do not include tests with your PR, we will ask you to do so before reviewing. If you open a PR and are unwilling to write tests, we may either write tests on your behalf, or close the PR. If you are uncomfortable writing tests or working with Jest (our current testing library), we would be happy to guide you. This requirement is not intended as a barrier to entry, but as a way for us to enforce and improve the stability of our long-lived library here. It also serves as a way to externally communicate how your change will (or perhaps how it will not) modify the behavior of MST. There's no documentation quite as good as a comprehensive test suite! ### Documentation Changes Good documentation is crucial for our users. If your contribution involves changes to the library's behavior, please update the documentation accordingly. Not every PR is necessarily going to require documentation updates, but we encourage you to consider touching up documentation related to your code changes. If you're unsure about where to make changes, feel free to reach out to us, and we'll be happy to guide you. When changing the API, commit the code changes first, then run `npm run build-docs` and commit the documentation separately. ### Submitting a Pull Request 1. Fork the MobX-State-Tree repository on GitHub to your own GitHub account. 2. Clone your fork to your local machine. 3. Create a new branch for your changes: `git checkout -b my-feature`. 4. Before starting, it's not a bad idea to `bun install && bun run build && bun run test:all` to check that your machine can run the full test suite to start. 5. Make your changes and ensure that all tests pass (including any new tests you have added). 6. Update the documentation if necessary. 7. Commit your changes: `git commit -m "Add my feature"`. Please consider [following conventional commit formatting](https://www.conventionalcommits.org/en/v1.0.0/). 8. Push your changes to your fork: `git push origin my-feature`. 9. Create a pull request on the MobX-State-Tree repository. We have a pull request template. If you fill that out and include examples of your changes, links to any issue(s) you're working on, and a good description of your PR, that would help us out a lot. If you skip those steps, we may ask you for clarification before reviewing your work. Our team will review your pull request as soon as possible and provide feedback. Please be patient, as it may take some time to review and merge your contribution. ## Reporting Bugs If you encounter a bug while using MobX-State-Tree, please help us by reporting it. To report a bug: 1. Check if the bug has already been reported by searching our [issue tracker](https://github.com/mobxjs/mobx-state-tree/issues). 2. If not, create a new issue, including as much detail as possible about the bug and steps to reproduce it. We have issue templates that will ask specific questions for you to help us understand the problem. ## Bigger PRs If you want to contribute a large or significant change to MST, we'd love to connect with you ahead of time to make sure it fits in with our overall road map, meets our stability requirements, and make sure you are set up for success. Please consider [asking around in the discussion forum](https://github.com/mobxjs/mobx-state-tree/discussions) if you have a big idea you want to implement, or if you want to work on some existing big ideas out therein the communit. ## Code of Conduct We strive to maintain a friendly and welcoming community. Please read and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) in all interactions within the MobX-State-Tree project. Thank you for considering contributing to MobX-State-Tree. Your contributions help us make our library better for everyone. We appreciate your support and look forward to collaborating with you! ================================================ FILE: Dockerfile ================================================ FROM node:8.11.4 WORKDIR /app/website EXPOSE 3000 35729 COPY ./docs /app/docs COPY ./website /app/website RUN yarn install CMD ["yarn", "start"] ================================================ FILE: ISSUE_TEMPLATE.md ================================================ **If your issue isn't a bug report, please consider using discussion threads instead of opening an issue: https://github.com/mobxjs/mobx-state-tree/discussions** ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Michel Weststrate Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ logo # mobx-state-tree [![npm version](https://badge.fury.io/js/mobx-state-tree.svg)](https://badge.fury.io/js/mobx-state-tree) [![CircleCI](https://circleci.com/gh/mobxjs/mobx-state-tree.svg?style=svg)](https://circleci.com/gh/mobxjs/mobx-state-tree) [![Have a question? Ask on GitHub Discussions!](https://img.shields.io/badge/Have%20a%20question%3F-Ask%20on%20GitHub%20Discussions!-blue)](https://github.com/mobxjs/mobx-state-tree/discussions) ## What is mobx-state-tree? Technically speaking, mobx-state-tree (also known as MST) is a state container system built on [MobX](https://github.com/mobxjs/mobx), a functional reactive state library. This may not mean much to you, and that’s okay. I’ll explain it like this: **MobX is a state management "engine", and MobX-State-Tree gives it structure and common tools you need for your app.** MST is valuable in a large team but also useful in smaller applications when you expect your code to scale rapidly. And if we compare it to Redux, MST offers better performance and much less boilerplate code than Redux! MobX is [one of the most popular Redux alternatives](https://2019.stateofjs.com/data-layer/mobx/) and is used (along with MobX-State-Tree) by companies worldwide. MST plays very well with TypeScript, React, and React Native, especially when paired with [mobx-react-lite](https://github.com/mobxjs/mobx/tree/main/packages/mobx-react-lite). It supports multiple stores, async actions and side effects, enables extremely targeted re-renders for React apps, and much more -- all in a package with _zero dependencies_ other than MobX itself. _Note: you don't need to know how to use MobX in order to use MST._ # Getting started See the [Getting started](https://mobx-state-tree.js.org/intro/getting-started) tutorial or follow the free [egghead.io course](https://egghead.io/courses/manage-application-state-with-mobx-state-tree). 👉 Official docs can be found at [http://mobx-state-tree.js.org/](http://mobx-state-tree.js.org/) ## Quick Code Example There's nothing quite like looking at some code to get a feel for a library. Check out this small example of an author and list of tweets by that author. ```js import { types } from "mobx-state-tree" // alternatively: import { t } from "mobx-state-tree" // Define a couple models const Author = types.model({ id: types.identifier, firstName: types.string, lastName: types.string }) const Tweet = types.model({ id: types.identifier, author: types.reference(Author), // stores just the `id` reference! body: types.string, timestamp: types.number }) // Define a store just like a model const RootStore = types.model({ authors: types.array(Author), tweets: types.array(Tweet) }) // Instantiate a couple model instances const jamon = Author.create({ id: "jamon", firstName: "Jamon", lastName: "Holmgren" }) const tweet = Tweet.create({ id: "1", author: jamon.id, // just the ID needed here body: "Hello world!", timestamp: Date.now() }) // Now instantiate the store! const rootStore = RootStore.create({ authors: [jamon], tweets: [tweet] }) // Ready to use in a React component, if that's your target. import { observer } from "mobx-react-lite" const MyComponent = observer((props) => { return
Hello, {rootStore.authors[0].firstName}!
}) // Note: since this component is "observed", any changes to rootStore.authors[0].firstName // will result in a re-render! If you're not using React, you can also "listen" to changes // using `onSnapshot`: https://mobx-state-tree.js.org/concepts/snapshots ``` ## Thanks! - [Michel Weststrate](https://twitter.com/mweststrate) for creating MobX, MobX-State-Tree, and MobX-React. - [Infinite Red](https://infinite.red) for supporting ongoing maintenance on MST. - [Mendix](https://mendix.com) for sponsoring and providing the opportunity to work on exploratory projects like MST. - [Dan Abramov](https://twitter.com/dan_abramov)'s work on [Redux](http://redux.js.org) has strongly influenced the idea of snapshots and transactional actions in MST. - [Giulio Canti](https://twitter.com/GiulioCanti)'s work on [tcomb](http://github.com/gcanti/tcomb) and type systems in general has strongly influenced the type system of MST. - All the early adopters encouraging to pursue this whole idea and proving it is something feasible. ================================================ FILE: SECURITY.md ================================================ ## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ================================================ FILE: __tests__/core/1525.test.ts ================================================ import { types, Instance } from "../../src/index" import { describe, it } from "bun:test" describe("1525. Model instance maybe fields becoming TypeScript optional fields when included in a types.union", () => { it("does not throw a typescript error", () => { const Model = types.model("myModel", { foo: types.string, bar: types.maybe(types.integer) }) const Store = types.model("store", { itemWithoutIssue: Model, itemWithIssue: types.union(types.literal("anotherValue"), Model) }) interface IModel extends Instance {} interface FunctionArgs { model1: IModel model2: IModel } const store = Store.create({ itemWithoutIssue: { foo: "works" }, itemWithIssue: { foo: "has ts error in a regression" } }) const f = (props: FunctionArgs) => {} const itemWithoutIssueModel = store.itemWithoutIssue const itemWithIssueModel = store.itemWithIssue === "anotherValue" ? null : store.itemWithIssue itemWithIssueModel && f({ model1: itemWithoutIssueModel, model2: itemWithIssueModel }) }) }) ================================================ FILE: __tests__/core/1664.test.ts ================================================ import { types as t } from "../../src/index" import { describe, test } from "bun:test" describe("1664. Array and model types are not inferred correctly when broken down into their components", () => { test("should not throw a typescript error", () => { // Simple concrete type with a creation type different than its instance type const date = t.custom({ name: "Date", fromSnapshot: snapshot => new Date(snapshot), toSnapshot: dt => dt.toISOString(), isTargetType: (val: unknown) => val instanceof Date, getValidationMessage: (snapshot: unknown) => typeof snapshot !== "string" || isNaN(Date.parse(snapshot)) ? `${snapshot} is not a valid Date string` : "" }) //Wrap the date type in an array type. IArrayType is a sub-interface of IType. const DateArray = t.array(date) //Pass the array type to t.union, which infers the component types as const LoadableDateArray = t.union(t.literal("loading"), DateArray) //Instantiate the type const lda = LoadableDateArray.create([]) //Try to use the array type as an instance if (lda !== "loading") { //Error: type of lda is essentially `(string | Date)[] | undefined` //The creation type has been mixed together with the instance type const dateArray: Date[] = lda } }) }) ================================================ FILE: __tests__/core/2230.test.ts ================================================ //github.com/mobxjs/mobx-state-tree/issues/2230 import { describe, test } from "bun:test" import { types, Instance } from "../../src/index" describe("2230 - type instantiation is excessively deep and possibly infinite", () => { test("does not happen", () => { const ModelProps = types .model({ prop01: "", prop02: "", prop03: "" }) .props({ prop11: "", prop12: "", prop13: "" }) .props({ prop21: "", prop22: "", prop23: "" }) .props({ prop31: "", prop32: "", prop33: "" }) .props({ prop41: "", prop42: "", prop43: "" }) .props({ prop51: "", prop52: "", prop53: "" }) .props({ prop61: "", prop62: "", prop63: "" }) .props({ prop71: "", prop72: "", prop73: "" }) .props({ prop81: "", prop82: "", prop83: "" }) .props({ prop91: "", prop92: "", prop93: "" }) interface IModelProps extends Instance {} const ModelVolatile = ModelProps.volatile(() => ({ vol01: null, vol02: null, vol03: null })) .volatile(() => ({ vol11: null, vol12: null, vol13: null })) .volatile(() => ({ vol21: null, vol22: null, vol23: null })) .volatile(() => ({ vol31: null, vol32: null, vol33: null })) .volatile(() => ({ vol41: null, vol42: null, vol43: null })) .volatile(() => ({ vol51: null, vol52: null, vol53: null })) .volatile(() => ({ vol61: null, vol62: null, vol63: null })) .volatile(() => ({ vol71: null, vol72: null, vol73: null })) .volatile(() => ({ vol81: null, vol82: null, vol83: null })) .volatile(() => ({ vol91: null, vol92: null, vol93: null })) interface IModelVolatile extends Instance {} const ModelViews = ModelVolatile.views((self: IModelVolatile) => ({ get vol01Var() { return self.vol01 } })) interface IModelViews extends Instance {} const Action1 = ModelViews.actions((self: IModelViews) => ({ getProp01(): string { return self.prop01 } })) interface IAction1 extends Instance {} const Action2 = Action1.actions((self: IAction1) => ({ getProp11(): string { return self.prop11 } })) interface IAction2 extends Instance {} const Action3 = Action2.actions((self: IAction2) => ({ getProp21(): string { return self.prop21 } })) interface IAction3 extends Instance {} const Action4 = Action3.actions((self: IAction3) => ({ getProp31(): string { return self.prop31 } })) interface IAction4 extends Instance {} const Action5 = Action4.actions((self: IAction4) => ({ getProp41(): string { return self.prop41 } })) .actions((self: IAction4) => ({ getProp51(): string { return self.prop51 } })) .actions((self: IAction4) => ({ getProp61(): string { return self.prop61 } })) .actions((self: IAction4) => ({ getProp71(): string { return self.prop71 } })) .actions((self: IAction4) => ({ getProp81(): string { return self.prop81 } })) .actions((self: IAction4) => ({ getProp91(): string { return self.prop91 } })) interface IAction5 extends Instance {} }) }) ================================================ FILE: __tests__/core/__snapshots__/async.test.ts.snap ================================================ // Bun Snapshot v1, https://goo.gl/fbAQLP exports[`can handle erroring actions 1`] = ` [ { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, { "allParentIds": [ 1, ], "args": [ "black", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_spawn", }, { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, ], "args": [ "black", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_throw", }, ] `; exports[`empty sequence works 1`] = ` [ { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, { "allParentIds": [ 1, ], "args": [ "black", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_spawn", }, { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_return", }, ] `; exports[`typings 1`] = ` [ { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, { "allParentIds": [ 1, ], "args": [ "black", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_spawn", }, { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, ], "args": [ "tea", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume_error", }, { "allParentIds": [ 1, ], "args": [ "biscuit", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_return", }, ] `; exports[`typings 2`] = ` [ { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, { "allParentIds": [ 1, ], "args": [ "black", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_spawn", }, { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, ], "args": [ "x", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume_error", }, { "allParentIds": [ 1, ], "args": [ "x", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_throw", }, ] `; exports[`can handle nested async actions when using decorate 1`] = ` [ { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, { "allParentIds": [ 1, ], "args": [ "black", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_spawn", }, { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, ], "args": [ "drinking coffee", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, ], "args": [ "awake", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_return", }, ] `; exports[`can handle nested async actions when using decorate 2`] = ` [ { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, { "allParentIds": [ 1, ], "args": [ "black", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_spawn", }, { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, 2, ], "args": [ "drinking black", ], "id": 3, "name": "uppercase", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, "parentId": 2, "rootId": 1, "type": "flow_spawn", }, { "allParentIds": [ 1, 2, ], "args": [ undefined, ], "id": 3, "name": "uppercase", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, "parentId": 2, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, 2, ], "args": [ "DRINKING BLACK", ], "id": 3, "name": "uppercase", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, "parentId": 2, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, 2, ], "args": [ "DRINKING BLACK", ], "id": 3, "name": "uppercase", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [ 1, ], "args": [ undefined, ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, "parentId": 2, "rootId": 1, "type": "flow_return", }, { "allParentIds": [ 1, ], "args": [ "DRINKING BLACK", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_resume", }, { "allParentIds": [ 1, ], "args": [ "DRINKING BLACK", ], "id": 2, "name": "fetchData", "parentActionEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentEvent": { "allParentIds": [], "args": [ "black", ], "id": 1, "name": "startFetch", "parentActionEvent": undefined, "parentEvent": undefined, "parentId": 0, "rootId": 1, "type": "action", }, "parentId": 1, "rootId": 1, "type": "flow_return", }, ] `; ================================================ FILE: __tests__/core/__snapshots__/custom-type.test.ts.snap ================================================ // Bun Snapshot v1, https://goo.gl/fbAQLP exports[`reassignments will work 1`] = ` [ { "balance": "2.5", "lastTransaction": null, }, { "balance": "3.5", "lastTransaction": null, }, { "balance": "4.5", "lastTransaction": null, }, { "balance": "4.5", "lastTransaction": "2.5", }, { "balance": "4.5", "lastTransaction": null, }, ] `; exports[`reassignments will work 2`] = ` [ { "op": "replace", "path": "/balance", "value": "2.5", }, { "op": "replace", "path": "/balance", "value": "3.5", }, { "op": "replace", "path": "/balance", "value": "4.5", }, { "op": "replace", "path": "/lastTransaction", "value": "2.5", }, { "op": "replace", "path": "/lastTransaction", "value": null, }, ] `; exports[`complex reassignments will work 1`] = ` [ { "balance": [ 2, 5, ], }, { "balance": [ 2, 5, ], }, { "balance": [ 3, 5, ], }, { "balance": [ 4, 5, ], }, ] `; exports[`complex reassignments will work 2`] = ` [ { "op": "replace", "path": "/balance", "value": [ 2, 5, ], }, { "op": "replace", "path": "/balance", "value": [ 2, 5, ], }, { "op": "replace", "path": "/balance", "value": [ 3, 5, ], }, { "op": "replace", "path": "/balance", "value": [ 4, 5, ], }, ] `; ================================================ FILE: __tests__/core/__snapshots__/reference-custom.test.ts.snap ================================================ // Bun Snapshot v1, https://goo.gl/fbAQLP exports[`it should support custom references - adv 1`] = ` [ "1", "2", "1", null, "3", ] `; exports[`it should support custom references - adv 2`] = ` [ { "selection": "Michel", "users": { "1": { "id": "1", "name": "Michel", }, "2": { "id": "2", "name": "Mattia", }, }, }, { "selection": "Mattia", "users": { "1": { "id": "1", "name": "Michel", }, "2": { "id": "2", "name": "Mattia", }, }, }, { "selection": "Michel", "users": { "1": { "id": "1", "name": "Michel", }, "2": { "id": "2", "name": "Mattia", }, }, }, { "selection": "Michel", "users": { "2": { "id": "2", "name": "Mattia", }, }, }, { "selection": "Michel", "users": { "2": { "id": "2", "name": "Mattia", }, "3": { "id": "3", "name": "Michel", }, }, }, ] `; exports[`it should support custom references - adv 3`] = ` [ { "op": "replace", "path": "/selection", "value": "Michel", }, { "op": "replace", "path": "/selection", "value": "Mattia", }, { "op": "replace", "path": "/selection", "value": "Michel", }, { "op": "remove", "path": "/users/1", }, { "op": "add", "path": "/users/3", "value": { "id": "3", "name": "Michel", }, }, ] `; exports[`it should support custom references - adv 4`] = ` [ { "op": "replace", "path": "/selection", "value": "Mattia", }, { "op": "replace", "path": "/selection", "value": "Michel", }, { "op": "replace", "path": "/selection", "value": "Mattia", }, { "op": "add", "path": "/users/1", "value": { "id": "1", "name": "Michel", }, }, { "op": "remove", "path": "/users/3", }, ] `; ================================================ FILE: __tests__/core/action.test.ts ================================================ import { configure } from "mobx" import { recordActions, types, getSnapshot, onAction, applyPatch, applySnapshot, addMiddleware, getRoot, cast, IMiddlewareEvent, ISerializedActionCall, Instance } from "../../src" import { expect, test } from "bun:test" /// Simple action replay and invocation const Task = types .model({ done: false }) .actions(self => { function toggle() { self.done = !self.done return self.done } return { toggle } }) test("it should be possible to invoke a simple action", () => { const t1 = Task.create() expect(t1.done).toBe(false) expect(t1.toggle()).toBe(true) expect(t1.done).toBe(true) }) test("it should be possible to record & replay a simple action", () => { const t1 = Task.create() const t2 = Task.create() expect(t1.done).toBe(false) expect(t2.done).toBe(false) const recorder = recordActions(t1) t1.toggle() t1.toggle() t1.toggle() expect(recorder.actions).toEqual([ { name: "toggle", path: "", args: [] }, { name: "toggle", path: "", args: [] }, { name: "toggle", path: "", args: [] } ]) recorder.replay(t2) expect(t2.done).toBe(true) }) test("applying patches should be recordable and replayable", () => { const t1 = Task.create() const t2 = Task.create() const recorder = recordActions(t1) expect(t1.done).toBe(false) applyPatch(t1, { op: "replace", path: "/done", value: true }) expect(t1.done).toBe(true) expect(recorder.actions).toEqual([ { name: "@APPLY_PATCHES", path: "", args: [[{ op: "replace", path: "/done", value: true }]] } ]) recorder.replay(t2) expect(t2.done).toBe(true) }) test("applying patches should be replacing the root store", () => { const t1 = Task.create() const recorder = recordActions(t1) expect(t1.done).toBe(false) applyPatch(t1, { op: "replace", path: "", value: { done: true } }) expect(t1.done).toBe(true) expect(recorder.actions).toEqual([ { name: "@APPLY_PATCHES", path: "", args: [[{ op: "replace", path: "", value: { done: true } }]] } ]) }) test("applying snapshots should be recordable and replayable", () => { const t1 = Task.create() const t2 = Task.create() const recorder = recordActions(t1) expect(t1.done).toBe(false) applySnapshot(t1, { done: true }) expect(t1.done).toBe(true) expect(recorder.actions).toEqual([ { name: "@APPLY_SNAPSHOT", path: "", args: [{ done: true }] } ]) recorder.replay(t2) expect(t2.done).toBe(true) }) // Complex actions const Customer = types.model("Customer", { id: types.identifierNumber, name: types.string }) const Order = types .model("Order", { customer: types.maybeNull(types.reference(Customer)) }) .actions(self => { function setCustomer(customer: Instance) { self.customer = customer } function noopSetCustomer(_: Instance) { // noop } return { setCustomer, noopSetCustomer } }) const OrderStore = types.model("OrderStore", { customers: types.array(Customer), orders: types.array(Order) }) function createTestStore() { const store = OrderStore.create({ customers: [{ id: 1, name: "Mattia" }], orders: [ { customer: null } ] }) onAction(store, () => {}) return store } test("it should not be possible to pass a complex object", () => { const store = createTestStore() const recorder = recordActions(store) expect(store.customers[0].name).toBe("Mattia") store.orders[0].setCustomer(store.customers[0]) expect(store.orders[0].customer!.name).toBe("Mattia") expect(store.orders[0].customer).toBe(store.customers[0]) expect(getSnapshot(store)).toEqual({ customers: [ { id: 1, name: "Mattia" } ], orders: [ { customer: 1 } ] }) expect(recorder.actions).toEqual([ { name: "setCustomer", path: "/orders/0", args: [{ $MST_UNSERIALIZABLE: true, type: "[MSTNode: Customer]" }] } ]) }) if (process.env.NODE_ENV !== "production") { test("it should not be possible to set the wrong type", () => { const store = createTestStore() expect(() => { store.orders[0].setCustomer(store.orders[0] as any) }).toThrow( "Error while converting to `(reference(Customer) | null)`:\n\n " + "value of type Order: is not assignable to type: `(reference(Customer) | null)`, expected an instance of `(reference(Customer) | null)` or a snapshot like `(reference(Customer) | null?)` instead." ) // wrong type! }) } test("it should not be possible to pass the element of another tree", () => { const store1 = createTestStore() const store2 = createTestStore() const recorder = recordActions(store2) store2.orders[0].setCustomer(store1.customers[0]) expect(recorder.actions).toEqual([ { name: "setCustomer", path: "/orders/0", args: [ { $MST_UNSERIALIZABLE: true, type: "[MSTNode: Customer]" } ] } ]) }) test("it should not be possible to pass an unserializable object", () => { const store = createTestStore() const circular = { a: null as any } circular.a = circular const recorder = recordActions(store) store.orders[0].noopSetCustomer(circular as any) store.orders[0].noopSetCustomer(Buffer.from("bla") as any) expect(recorder.actions).toEqual([ { args: [ { $MST_UNSERIALIZABLE: true, type: "TypeError: JSON.stringify cannot serialize cyclic structures." } ], name: "noopSetCustomer", path: "/orders/0" }, { args: [ { $MST_UNSERIALIZABLE: true, type: "[object Buffer]" } ], name: "noopSetCustomer", path: "/orders/0" } ]) }) test("it should be possible to pass a complex plain object", () => { const t1 = Task.create() const t2 = Task.create() const recorder = recordActions(t1) ;(t1 as any).toggle({ bla: ["nuff", ["said"]] }) // nonsense, but serializable! expect(recorder.actions).toEqual([ { name: "toggle", path: "", args: [{ bla: ["nuff", ["said"]] }] } ]) recorder.replay(t2) expect(t2.done).toBe(true) }) test("action should be bound", () => { const task = Task.create() const f = task.toggle expect(f()).toBe(true) expect(task.done).toBe(true) }) test("snapshot should be available and updated during an action", () => { const Model = types .model({ x: types.number }) .actions(self => { function inc() { self.x += 1 const res = getSnapshot(self).x self.x += 1 return res } return { inc } }) const a = Model.create({ x: 2 }) expect(a.inc()).toBe(3) expect(a.x).toBe(4) expect(getSnapshot(a).x).toBe(4) }) test("indirectly called private functions should be able to modify state", () => { const Model = types .model({ x: 3 }) .actions(self => { function incrementBy(delta: number) { self.x += delta } return { inc() { incrementBy(1) }, dec() { incrementBy(-1) } } }) const cnt = Model.create() expect(cnt.x).toBe(3) cnt.dec() expect(cnt.x).toBe(2) expect((cnt as any).incrementBy).toBe(undefined) }) test("volatile state survives reonciliation", () => { const Model = types.model({ x: 3 }).actions(self => { let incrementor = 1 return { setIncrementor(value: number) { incrementor = value }, inc() { self.x += incrementor } } }) const Store = types.model({ cnt: types.optional(Model, {}) }) const store = Store.create() store.cnt.inc() expect(store.cnt.x).toBe(4) store.cnt.setIncrementor(3) store.cnt.inc() expect(store.cnt.x).toBe(7) applySnapshot(store, { cnt: { x: 2 } }) expect(store.cnt.x).toBe(2) store.cnt.inc() expect(store.cnt.x).toBe(5) // incrementor was not lost }) test("middleware events are correct", () => { configure({ useProxies: "never" }) const A = types.model({}).actions(self => ({ a(x: number) { return this.b(x * 2) }, b(y: number) { return y + 1 } })) const a = A.create() const events: IMiddlewareEvent[] = [] addMiddleware(a, function (call, next) { events.push(call) return next(call) }) a.a(7) const event1 = { args: [7], context: {}, id: 1, name: "a", parentId: 0, rootId: 1, allParentIds: [], tree: {}, type: "action", parentEvent: undefined, parentActionEvent: undefined } as IMiddlewareEvent const event2 = { args: [14], context: {}, id: 2, name: "b", parentId: 1, rootId: 1, allParentIds: [1], tree: {}, type: "action", parentEvent: event1, parentActionEvent: event1 } as IMiddlewareEvent expect(events).toEqual([event1, event2]) }) test("actions are mockable", () => { configure({ useProxies: "never" }) const M = types .model() .actions(self => ({ method(): number { return 3 } })) .views(self => ({ view(): number { return 3 } })) const m = M.create() if (process.env.NODE_ENV === "production") { expect(() => { m.method = function () { return 3 } }).toThrow(TypeError) expect(() => { m.view = function () { return 3 } }).toThrow(TypeError) } else { m.method = function () { return 4 } expect(m.method()).toBe(4) m.view = function () { return 4 } expect(m.view()).toBe(4) } }) test("after attach action should work correctly", () => { const Todo = types .model({ title: "test" }) .actions(self => ({ remove() { getRoot(self).remove(cast(self)) } })) const S = types .model({ todos: types.array(Todo) }) .actions(self => ({ remove(todo: Instance) { self.todos.remove(todo) } })) const s = S.create({ todos: [{ title: "todo" }] }) const events: ISerializedActionCall[] = [] onAction( s, call => { events.push(call) }, true ) s.todos[0].remove() expect(events).toEqual([ { args: [], name: "remove", path: "/todos/0" } ]) }) ================================================ FILE: __tests__/core/actionTrackingMiddleware2.test.ts ================================================ import { addMiddleware, createActionTrackingMiddleware2, types, flow, IActionTrackingMiddleware2Call } from "../../src" import { expect, it, test } from "bun:test" function createTestMiddleware(m: any, actionName: string, value: number, calls: string[]) { function checkCall(call: IActionTrackingMiddleware2Call) { expect(call.name).toBe(actionName) expect(call.args).toEqual([value]) expect(call.context).toBe(m) expect(call.env).toBe(call.id) } const mware = createActionTrackingMiddleware2({ filter(call) { return call.name === actionName }, onStart(call) { call.env = call.id // just to check env is copied properly down calls.push(`${call.name} (${call.id}) - onStart`) checkCall(call) }, onFinish(call, error) { calls.push(`${call.name} (${call.id}) - onFinish (error: ${!!error})`) checkCall(call) } }) addMiddleware(m, mware, false) } async function doTest(m: any, mode: "success" | "fail") { const calls: string[] = [] createTestMiddleware(m, "setX", 10, calls) createTestMiddleware(m, "setY", 9, calls) try { await m.setZ(8) // -> setY(9) -> setX(10) if (mode === "fail") { expect().fail("should have failed") } } catch (e) { if (mode === "fail") { expect(e).toBe("error") } else { throw e // fail("should have succeeded") } } return calls } async function syncTest(mode: "success" | "fail") { const M = types .model({ x: 1, y: 2, z: 3 }) .actions(self => ({ setX(v: number) { self.x = v if (mode === "fail") { throw "error" } }, setY(v: number) { self.y = v this.setX(v + 1) }, setZ(v: number) { self.z = v this.setY(v + 1) } })) const m = M.create() const calls = await doTest(m, mode) if (mode === "success") { expect(calls).toEqual([ "setY (2) - onStart", "setX (3) - onStart", "setX (3) - onFinish (error: false)", "setY (2) - onFinish (error: false)" ]) } else { expect(calls).toEqual([ "setY (5) - onStart", "setX (6) - onStart", "setX (6) - onFinish (error: true)", "setY (5) - onFinish (error: true)" ]) } } /** * This test checks that the middleware is called and */ test("sync action", async () => { await syncTest("success") await syncTest("fail") }) async function flowTest(mode: "success" | "fail") { const _subFlow = flow(function* subFlow() { yield Promise.resolve() }) const M = types .model({ x: 1, y: 2, z: 3 }) .actions(self => ({ setX: flow(function* flowSetX(v: number) { yield Promise.resolve() yield _subFlow() self.x = v if (mode === "fail") { throw "error" } }), setY: flow(function* flowSetY(v: number) { self.y = v yield (self as any).setX(v + 1) }), setZ: flow(function* flowSetZ(v: number) { self.z = v yield (self as any).setY(v + 1) }) })) const m = M.create() const calls = await doTest(m, mode) if (mode === "success") { expect(calls).toEqual([ "setY (3) - onStart", "setX (5) - onStart", "setX (5) - onFinish (error: false)", "setY (3) - onFinish (error: false)" ]) } else { expect(calls).toEqual([ "setY (10) - onStart", "setX (12) - onStart", "setX (12) - onFinish (error: true)", "setY (10) - onFinish (error: true)" ]) } } test("flow action", async () => { await flowTest("success") await flowTest("fail") }) test("#1250", async () => { const M = types .model({ x: 0, y: 0 }) .actions(self => ({ setX: flow(function* () { self.x = 10 yield new Promise(resolve => setTimeout(resolve, 10)) }), setY() { self.y = 10 } })) const calls: string[] = [] const mware = createActionTrackingMiddleware2({ filter(call) { calls.push( `${call.name} (${call.id}) <- (${call.parentCall && call.parentCall.id}) - filter` ) return true }, onStart(call) { calls.push( `${call.name} (${call.id}) <- (${call.parentCall && call.parentCall.id}) - onStart` ) }, onFinish(call, error) { calls.push( `${call.name} (${call.id}) <- (${ call.parentCall && call.parentCall.id }) - onFinish (error: ${!!error})` ) } }) const model = M.create({}) addMiddleware(model, mware, false) expect(model.x).toBe(0) expect(model.y).toBe(0) expect(calls).toEqual([]) const p = model.setX() expect(model.x).toBe(10) expect(model.y).toBe(0) expect(calls).toEqual(["setX (1) <- (undefined) - filter", "setX (1) <- (undefined) - onStart"]) calls.length = 0 await new Promise(r => setTimeout(() => { model.setY() r() }, 5) ) expect(model.x).toBe(10) expect(model.y).toBe(10) expect(calls).toEqual([ "setY (3) <- (undefined) - filter", "setY (3) <- (undefined) - onStart", "setY (3) <- (undefined) - onFinish (error: false)" ]) calls.length = 0 await p expect(model.x).toBe(10) expect(model.y).toBe(10) expect(calls).toEqual(["setX (1) <- (undefined) - onFinish (error: false)"]) calls.length = 0 }) /** * Test that when createActionTrackingMiddleware2 is called with valid hooks and a synchronous action, it runs onStart and onFinish hooks. */ test("successful execution", () => { const M = types.model({}).actions(self => ({ test() {} })) const calls: string[] = [] const mware = createActionTrackingMiddleware2({ filter(call) { calls.push(`${call.name} - filter`) return true }, onStart(call) { calls.push(`${call.name} - onStart`) }, onFinish(call, error) { calls.push(`${call.name} - onFinish (error: ${!!error})`) } }) const model = M.create({}) addMiddleware(model, mware, false) model.test() expect(calls).toEqual(["test - filter", "test - onStart", "test - onFinish (error: false)"]) }) /** * Test that when createActionTrackingMiddleware2 is called with valid hooks and an asynchronous action, it runs onStart and onFinish hooks. */ test("successful execution with async action", async () => { const M = types.model({}).actions(self => ({ async test() {} })) const calls: string[] = [] const mware = createActionTrackingMiddleware2({ filter(call) { calls.push(`${call.name} - filter`) return true }, onStart(call) { calls.push(`${call.name} - onStart`) }, onFinish(call, error) { calls.push(`${call.name} - onFinish (error: ${!!error})`) } }) const model = M.create({}) addMiddleware(model, mware, false) await model.test() expect(calls).toEqual(["test - filter", "test - onStart", "test - onFinish (error: false)"]) }) /** * Test that when the filter returns true, the action is tracked. We check * this by checking that the onStart and onFinish hooks are called for `runThisOne`, * which is the name provided to the `filter` function. */ it("calls onStart and onFinish hooks for actions that pass the filter", () => { const M2 = types.model({}).actions(self => ({ trackThisOne() {}, doNotTrackThisOne() {} })) const calls: string[] = [] const mware2 = createActionTrackingMiddleware2({ filter(call) { return call.name === "trackThisOne" }, onStart(call) { calls.push(`${call.name} - onStart`) }, onFinish(call, error) { calls.push(`${call.name} - onFinish (error: ${!!error})`) } }) const model2 = M2.create({}) addMiddleware(model2, mware2, false) model2.trackThisOne() // We call this action to prove that it is not tracked since it fails - there's also a test for this below. model2.doNotTrackThisOne() expect(calls).toEqual(["trackThisOne - onStart", "trackThisOne - onFinish (error: false)"]) }) /** * Test that when the filter returns false, the action is not tracked. We check * this by checking that the onStart and onFinish hooks are not called for `doNotTrackThisOne`, */ it("does not call onStart and onFinish hooks for actions that do not pass the filter", () => { const M = types.model({}).actions(self => ({ trackThisOne() {}, doNotTrackThisOne() {} })) const calls: string[] = [] const mware = createActionTrackingMiddleware2({ filter(call) { return call.name === "trackThisOne" }, onStart(call) { calls.push(`${call.name} - onStart`) }, onFinish(call, error) { calls.push(`${call.name} - onFinish (error: ${!!error})`) } }) const model = M.create({}) addMiddleware(model, mware, false) model.doNotTrackThisOne() expect(calls).toEqual([]) }) /** * Test that parent actions and child actions have the expected order of operations - * if we had an action `a` that called an action `b1`, then `b2` inside `a`, the flow would be: * * - `filter(a)` * - `onStart(a)` * - `filter(b1)` * - `onStart(b1)` * - `onFinish(b1)` * - `filter(b2)` * - `onStart(b2)` * - `onFinish(b2)` * - `onFinish(a)` * * See https://mobx-state-tree.js.org/API/#createactiontrackingmiddleware2 */ test("complete in the expected recursive order", () => { const M = types .model({}) .actions(self => ({ childAction1() {}, childAction2() {} })) .actions(self => ({ parentAction() { self.childAction1() self.childAction2() } })) const calls: string[] = [] const mware = createActionTrackingMiddleware2({ filter(call) { calls.push(`${call.name} - filter`) return true }, onStart(call) { calls.push(`${call.name} - onStart`) }, onFinish(call, error) { calls.push(`${call.name} - onFinish (error: ${!!error})`) } }) const model = M.create({}) addMiddleware(model, mware, false) model.parentAction() expect(calls).toEqual([ "parentAction - filter", "parentAction - onStart", "childAction1 - filter", "childAction1 - onStart", "childAction1 - onFinish (error: false)", "childAction2 - filter", "childAction2 - onStart", "childAction2 - onFinish (error: false)", "parentAction - onFinish (error: false)" ]) }) ================================================ FILE: __tests__/core/api.test.ts ================================================ import { expect, test } from "bun:test" import { readFileSync } from "fs" import * as mst from "../../src" function stringToArray(s: string): string[] { return s.split(",").map(str => str.trim()) } const METHODS_AND_INTERNAL_TYPES = stringToArray(` typecheck, escapeJsonPath, unescapeJsonPath, joinJsonPath, splitJsonPath, decorate, addMiddleware, isStateTreeNode, flow, castFlowReturn, applyAction, onAction, recordActions, createActionTrackingMiddleware, createActionTrackingMiddleware2, setLivelinessChecking, getLivelinessChecking, getType, getChildType, onPatch, onSnapshot, applyPatch, recordPatches, protect, unprotect, isProtected, applySnapshot, getSnapshot, hasParent, getParent, hasParentOfType, getParentOfType, getRoot, getPath, getPathParts, isRoot, resolvePath, resolveIdentifier, getIdentifier, tryResolve, getRelativePath, clone, detach, destroy, isAlive, addDisposer, getEnv, hasEnv, walk, getMembers, getPropertyMembers, cast, castToSnapshot, castToReferenceSnapshot, isType, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isReferenceType, isRefinementType, isUnionType, isValidReference, tryReference, types, t, getNodeId, getRunningActionContext, isActionContextChildOf, isActionContextThisOrChildOf, toGeneratorFunction, toGenerator `) const DEPRECATED_METHODS_AND_INTERNAL_TYPES = stringToArray(` setLivelynessChecking, process `) const METHODS = METHODS_AND_INTERNAL_TYPES.filter(s => s[0].toLowerCase() === s[0]) const INTERNAL_TYPES = METHODS_AND_INTERNAL_TYPES.filter(s => s[0].toUpperCase() === s[0]) const TYPES = stringToArray(` enumeration, model, compose, custom, reference, safeReference, union, optional, literal, maybe, maybeNull, refinement, string, boolean, number, integer, float, finite, bigint, Date, map, array, frozen, identifier, identifierNumber, late, lazy, undefined, null, snapshotProcessor `) test("correct api exposed", () => { expect( Object.keys(mst) .sort() .filter(key => (mst as any)[key] !== undefined) // filter out interfaces .filter(s => !DEPRECATED_METHODS_AND_INTERNAL_TYPES.includes(s)) ).toEqual([...METHODS, ...INTERNAL_TYPES].sort()) }) test("correct types exposed", () => { expect(Object.keys(mst.types).sort()).toEqual(TYPES.sort()) }) test("types also exposed on t module", () => { expect(Object.keys(mst.t).sort()).toEqual(TYPES.sort()) }) test("all methods mentioned in API docs", () => { const apimd = readFileSync(__dirname + "/../../docs/API/index.md", "utf8") const missing = TYPES.map(type => "types." + type).filter( identifier => apimd.indexOf(identifier) === -1 ) missing.push( ...METHODS.filter(identifier => apimd.indexOf("#" + identifier.toLowerCase()) === -1) ) expect(missing).toEqual(["types.lazy", "types"]) }) test("only accepted dependencies", () => { const validDeps: string[] = ["ts-essentials"] const deps = JSON.parse(readFileSync(__dirname + "/../../package.json", "utf8")).dependencies || {} const depNames = Object.keys(deps) || [] expect(depNames.sort()).toEqual(validDeps.sort()) }) ================================================ FILE: __tests__/core/array.test.ts ================================================ import { unprotect, onSnapshot, onPatch, clone, isAlive, applyPatch, getPath, applySnapshot, getSnapshot, types, IJsonPatch, setLivelinessChecking, detach, cast } from "../../src" import { observable, autorun, configure } from "mobx" import { expect, test } from "bun:test" const createTestFactories = () => { const ItemFactory = types.optional( types.model({ to: "world" }), {} ) const Factory = types.array(ItemFactory) return { Factory, ItemFactory } } // === FACTORY TESTS === test("it should create a factory", () => { const { Factory } = createTestFactories() expect(getSnapshot(Factory.create())).toEqual([]) }) test("it should succeed if not optional and no default provided", () => { const Factory = types.array(types.string) expect(getSnapshot(Factory.create())).toEqual([]) }) test("it should restore the state from the snapshot", () => { configure({ useProxies: "never" }) const { Factory } = createTestFactories() const instance = Factory.create([{ to: "universe" }]) expect(getSnapshot(instance)).toEqual([{ to: "universe" }]) expect("" + instance).toBe("AnonymousModel@/0") // just the normal to string }) // === SNAPSHOT TESTS === test("it should emit snapshots", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) let snapshots: (typeof Factory.SnapshotType)[] = [] onSnapshot(doc, snapshot => snapshots.push(snapshot)) doc.push(ItemFactory.create()) expect(snapshots).toEqual([[{ to: "world" }]]) }) test("it should apply snapshots", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() applySnapshot(doc, [{ to: "universe" }]) expect(getSnapshot(doc)).toEqual([{ to: "universe" }]) }) test("it should return a snapshot", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.push(ItemFactory.create()) expect(getSnapshot(doc)).toEqual([{ to: "world" }]) }) // === PATCHES TESTS === test("it should emit add patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) doc.push(ItemFactory.create({ to: "universe" })) expect(patches).toEqual([{ op: "add", path: "/0", value: { to: "universe" } }]) }) test("it should apply an add patch", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() applyPatch(doc, { op: "add", path: "/0", value: { to: "universe" } }) expect(getSnapshot(doc)).toEqual([{ to: "universe" }]) }) test("it should emit update patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.push(ItemFactory.create()) let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) doc[0] = ItemFactory.create({ to: "universe" }) expect(patches).toEqual([{ op: "replace", path: "/0", value: { to: "universe" } }]) }) test("it should apply an update patch", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() applyPatch(doc, { op: "replace", path: "/0", value: { to: "universe" } }) expect(getSnapshot(doc)).toEqual([{ to: "universe" }]) }) test("it should emit remove patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.push(ItemFactory.create()) let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) doc.splice(0) expect(patches).toEqual([{ op: "replace", path: "", value: [] }]) }) test("it should apply a remove patch", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.push(ItemFactory.create()) doc.push(ItemFactory.create({ to: "universe" })) applyPatch(doc, { op: "remove", path: "/0" }) expect(getSnapshot(doc)).toEqual([{ to: "universe" }]) }) test("it should apply patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() applyPatch(doc, [ { op: "add", path: "/0", value: { to: "mars" } }, { op: "replace", path: "/0", value: { to: "universe" } } ]) expect(getSnapshot(doc)).toEqual([{ to: "universe" }]) }) // === TYPE CHECKS === test("it should check the type correctly", () => { const { Factory } = createTestFactories() const doc = Factory.create() expect(Factory.is(doc)).toEqual(true) expect(Factory.is([])).toEqual(true) expect(Factory.is({})).toEqual(false) expect(Factory.is([{ to: "mars" }])).toEqual(true) expect(Factory.is([{ wrongKey: true }])).toEqual(true) expect(Factory.is([{ to: true }])).toEqual(false) }) test("paths shoud remain correct when splicing", () => { const Task = types.model("Task", { done: false }) const store = types .model({ todos: types.array(Task) }) .create({ todos: [{}] }) unprotect(store) expect(store.todos.map(getPath)).toEqual(["/todos/0"]) store.todos.push({}) expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"]) store.todos.unshift({}) expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"]) store.todos.splice(0, 2) expect(store.todos.map(getPath)).toEqual(["/todos/0"]) store.todos.splice(0, 1, {}, {}, {}) expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"]) store.todos.remove(store.todos[1]) expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"]) }) test("items should be reconciled correctly when splicing - 1", () => { configure({ useProxies: "never" }) const Task = types.model("Task", { x: types.string }) const a = Task.create({ x: "a" }), b = Task.create({ x: "b" }), c = Task.create({ x: "c" }), d = Task.create({ x: "d" }) const store = types .model({ todos: types.array(Task) }) .create({ todos: [a] }) unprotect(store) expect(store.todos.slice()).toEqual([a]) expect(isAlive(a)).toBe(true) store.todos.push(b) expect(store.todos.slice()).toEqual([a, b]) store.todos.unshift(c) expect(store.todos.slice()).toEqual([c, a, b]) store.todos.splice(0, 2) expect(store.todos.slice()).toEqual([b]) expect(isAlive(a)).toBe(false) expect(isAlive(b)).toBe(true) expect(isAlive(c)).toBe(false) setLivelinessChecking("error") expect(() => store.todos.splice(0, 1, a, c, d)).toThrow( "You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'Task', Path upon death: '/todos/1', Subpath: '', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree." ) store.todos.splice(0, 1, clone(a), clone(c), clone(d)) expect(store.todos.map(_ => _.x)).toEqual(["a", "c", "d"]) }) test("items should be reconciled correctly when splicing - 2", () => { const Task = types.model("Task", { x: types.string }) const a = Task.create({ x: "a" }), b = Task.create({ x: "b" }), c = Task.create({ x: "c" }), d = Task.create({ x: "d" }) const store = types .model({ todos: types.array(Task) }) .create({ todos: [a, b, c, d] }) unprotect(store) store.todos.splice(2, 1, { x: "e" }, { x: "f" }) // becomes, a, b, e, f, d expect(store.todos.length).toBe(5) expect(store.todos[0] === a).toBe(true) expect(store.todos[1] === b).toBe(true) expect(store.todos[2] !== c).toBe(true) expect(store.todos[2].x).toBe("e") expect(store.todos[3] !== d).toBe(true) expect(store.todos[3].x).toBe("f") expect(store.todos[4] === d).toBe(true) // preserved and moved expect(store.todos[4].x).toBe("d") expect(store.todos.map(getPath)).toEqual([ "/todos/0", "/todos/1", "/todos/2", "/todos/3", "/todos/4" ]) store.todos.splice(1, 3, { x: "g" }) // becomes a, g, d expect(store.todos.length).toBe(3) expect(store.todos[0] === a).toBe(true) expect(store.todos[1].x).toBe("g") expect(store.todos[2].x).toBe("d") expect(store.todos[1] !== b).toBe(true) expect(store.todos[2] === d).toBe(true) // still original d expect(store.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"]) }) test("it should reconciliate keyed instances correctly", () => { const Store = types.model({ todos: types.optional( types.array( types.model("Task", { id: types.identifier, task: "", done: false }) ), [] ) }) const store = Store.create({ todos: [ { id: "1", task: "coffee", done: false }, { id: "2", task: "tea", done: false }, { id: "3", task: "biscuit", done: false } ] }) expect(store.todos.map(todo => todo.task)).toEqual(["coffee", "tea", "biscuit"]) expect(store.todos.map(todo => todo.done)).toEqual([false, false, false]) expect(store.todos.map(todo => todo.id)).toEqual(["1", "2", "3"]) const coffee = store.todos[0] const tea = store.todos[1] const biscuit = store.todos[2] applySnapshot(store, { todos: [ { id: "2", task: "Tee", done: true }, { id: "1", task: "coffee", done: true }, { id: "4", task: "biscuit", done: false }, { id: "5", task: "stuffz", done: false } ] }) expect(store.todos.map(todo => todo.task)).toEqual(["Tee", "coffee", "biscuit", "stuffz"]) expect(store.todos.map(todo => todo.done)).toEqual([true, true, false, false]) expect(store.todos.map(todo => todo.id)).toEqual(["2", "1", "4", "5"]) expect(store.todos[0] === tea).toBe(true) expect(store.todos[1] === coffee).toBe(true) expect(store.todos[2] === biscuit).toBe(false) }) test("it correctly reconciliate when swapping", () => { const Task = types.model("Task", {}) const Store = types.model({ todos: types.optional(types.array(Task), []) }) const s = Store.create() unprotect(s) const a = Task.create() const b = Task.create() s.todos.push(a, b) s.todos.replace([b, a]) expect(s.todos[0] === b).toBe(true) expect(s.todos[1] === a).toBe(true) expect(s.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"]) }) test("it correctly reconciliate when swapping using snapshots", () => { const Task = types.model("Task", {}) const Store = types.model({ todos: types.array(Task) }) const s = Store.create() unprotect(s) const a = Task.create() const b = Task.create() s.todos.push(a, b) s.todos.replace([getSnapshot(b), getSnapshot(a)]) expect(s.todos[0] === b).toBe(true) expect(s.todos[1] === a).toBe(true) expect(s.todos.map(getPath)).toEqual(["/todos/0", "/todos/1"]) s.todos.push({}) expect(s.todos[0] === b).toBe(true) expect(s.todos[1] === a).toBe(true) expect(s.todos.map(getPath)).toEqual(["/todos/0", "/todos/1", "/todos/2"]) }) test("it should not be allowed to add the same item twice to the same store", () => { const Task = types.model("Task", {}) const Store = types.model({ todos: types.optional(types.array(Task), []) }) const s = Store.create() unprotect(s) const a = Task.create() s.todos.push(a) expect(() => { s.todos.push(a) }).toThrow( "Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '/todos/1', but it lives already at '/todos/0'" ) const b = Task.create() expect(() => { s.todos.push(b, b) }).toThrow( "Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '/todos/2', but it lives already at '/todos/1'" ) }) test("it should support observable arrays", () => { const TestArray = types.array(types.number) const testArray = TestArray.create(observable([1, 2])) expect(testArray[0] === 1).toBe(true) expect(testArray.length === 2).toBe(true) expect(Array.isArray(testArray.slice())).toBe(true) }) test("it should support observable arrays, array should be real when useProxies eq 'always'", () => { const TestArray = types.array(types.number) const testArray = TestArray.create(observable([1, 2])) expect(testArray[0] === 1).toBe(true) expect(testArray.length === 2).toBe(true) expect(Array.isArray(testArray)).toBe(true) }) test("it should support observable arrays, array should be not real when useProxies eq 'never'", () => { configure({ useProxies: "never" }) const TestArray = types.array(types.number) const testArray = TestArray.create(observable([1, 2])) expect(testArray[0] === 1).toBe(true) expect(testArray.length === 2).toBe(true) expect(Array.isArray(testArray.slice())).toBe(true) expect(Array.isArray(testArray)).toBe(false) }) test("it should correctly handle re-adding of the same objects", () => { const Store = types .model("Task", { objects: types.array(types.maybe(types.frozen())) }) .actions(self => ({ setObjects(objects: {}[]) { self.objects.replace(objects) } })) const store = Store.create({ objects: [] }) expect(store.objects.slice()).toEqual([]) const someObject = {} store.setObjects([someObject]) expect(store.objects.slice()).toEqual([someObject]) store.setObjects([someObject]) expect(store.objects.slice()).toEqual([someObject]) }) test("it should work correctly for splicing primitive array", () => { const store = types.array(types.number).create([1, 2, 3]) unprotect(store) store.splice(0, 1) expect(store.slice()).toEqual([2, 3]) store.unshift(1) expect(store.slice()).toEqual([1, 2, 3]) store.replace([4, 5]) expect(store.slice()).toEqual([4, 5]) store.clear() expect(store.slice()).toEqual([]) }) test("it should keep unchanged for structrual equalled snapshot", () => { const Store = types.model({ todos: types.array( types.model("Task", { id: types.identifier, task: "", done: false }) ), numbers: types.array(types.number) }) const store = Store.create({ todos: [ { id: "1", task: "coffee", done: false }, { id: "2", task: "tea", done: false }, { id: "3", task: "biscuit", done: false } ], numbers: [1, 2, 3] }) const values: boolean[][] = [] autorun(() => { values.push(store.todos.map(todo => todo.done)) }) applySnapshot(store.todos, [ { id: "1", task: "coffee", done: false }, { id: "2", task: "tea", done: false }, { id: "3", task: "biscuit", done: true } ]) applySnapshot(store.todos, [ { id: "1", task: "coffee", done: false }, { id: "2", task: "tea", done: false }, { id: "3", task: "biscuit", done: true } ]) expect(values).toEqual([ [false, false, false], [false, false, true] ]) const values1: number[][] = [] autorun(() => { values1.push(store.numbers.slice()) }) applySnapshot(store.numbers, [1, 2, 4]) applySnapshot(store.numbers, [1, 2, 4]) expect(values1).toEqual([ [1, 2, 3], [1, 2, 4] ]) }) // === OPERATIONS TESTS === test("#1105 - it should return pop/shift'ed values for scalar arrays", () => { const ScalarArray = types .model({ array: types.array(types.number) }) .actions(self => { return { shift() { return self.array.shift() } } }) const test = ScalarArray.create({ array: [3, 5] }) expect(test.shift()).toEqual(3) expect(test.shift()).toEqual(5) }) test("it should return pop/shift'ed values for object arrays", () => { const TestObject = types.model({ id: types.string }) const ObjectArray = types .model({ array: types.array(TestObject) }) .actions(self => { return { shift() { return self.array.shift() }, pop() { return self.array.pop() } } }) const test = ObjectArray.create({ array: [{ id: "foo" }, { id: "mid" }, { id: "bar" }] }) const foo = test.shift()! expect(isAlive(foo)).toBe(false) const bar = test.pop()! expect(isAlive(bar)).toBe(false) // we have to use clone or getSnapshot to access dead nodes data expect(clone(foo)).toEqual({ id: "foo" }) expect(getSnapshot(bar)).toEqual({ id: "bar" }) }) test("#1173 - detaching an array should not eliminate its children", () => { const M = types.model({}) const AM = types.array(M) const Store = types.model({ items: AM }) const s = Store.create({ items: [{}, {}, {}] }) const n0 = s.items[0] unprotect(s) const detachedItems = detach(s.items) expect(s.items).not.toBe(detachedItems) expect(s.items.length).toBe(0) expect(detachedItems.length).toBe(3) expect(detachedItems[0]).toBe(n0) }) test("initializing an array instance from another array instance should end up in the same instance", () => { const A = types.array(types.number) const a1 = A.create([1, 2, 3]) const a2 = A.create(a1) expect(a1).toBe(a2) expect(getSnapshot(a1)).toEqual([1, 2, 3]) }) test("assigning filtered instances works", () => { const Task = types.model("Task", { done: false }) const store = types .model({ todos: types.array(Task) }) .actions(self => ({ clearFinishedTodos() { self.todos = cast(self.todos.filter(todo => !todo.done)) } })) .create({ todos: [{ done: true }, { done: false }, { done: true }] }) expect(store.todos.length).toBe(3) const done = store.todos.filter(t => t.done) const notDone = store.todos.filter(t => !t.done) expect(store.todos.every(t => isAlive(t))) store.clearFinishedTodos() expect(store.todos.length).toBe(1) expect(store.todos[0]).toBe(notDone[0]) expect(done.every(t => !isAlive(t))).toBe(true) expect(notDone.every(t => isAlive(t))).toBe(true) }) test("#1676 - should accept read-only arrays", () => { const ArrayType = types.array(types.string) const data = ["foo", "bar"] as const const instance = ArrayType.create(data) expect(getSnapshot(instance)).toEqual(["foo", "bar"]) }) ================================================ FILE: __tests__/core/async.test.ts ================================================ import { configure, reaction } from "mobx" import { addMiddleware, decorate, destroy, flow, IMiddlewareEvent, IMiddlewareEventType, IMiddlewareHandler, recordActions, toGenerator, // TODO: export IRawActionCall toGeneratorFunction, types } from "../../src" import { expect, test } from "bun:test" import type { Writable } from "ts-essentials" function delay(time: number, value: TV, shouldThrow = false): Promise { return new Promise((resolve, reject) => { setTimeout(() => { if (shouldThrow) reject(value) else resolve(value) }, time) }) } async function testCoffeeTodo( generator: ( self: any ) => (str: string) => Generator, string | void | undefined, undefined>, shouldError: boolean, resultValue: string | undefined, producedCoffees: any[] ) { const Todo = types .model({ title: "get coffee" }) .actions(self => ({ startFetch: flow(generator(self)) })) const events: IMiddlewareEvent[] = [] const t1 = Todo.create({}) addMiddleware(t1, (c, next) => { events.push(c) return next(c) }) const coffees: any[] = [] reaction( () => t1.title, coffee => coffees.push(coffee) ) try { configure({ enforceActions: "observed" }) const result = await t1.startFetch("black") expect(shouldError).toBe(false) expect(result).toBe(resultValue) } catch (error) { expect(shouldError).toBe(true) } finally { configure({ enforceActions: "never" }) } expect(coffees).toEqual(producedCoffees) const filtered = filterRelevantStuff(events) expect(filtered).toMatchSnapshot() } test("flow happens in single ticks", async () => { const X = types .model({ y: 1 }) .actions(self => ({ p: flow(function* () { self.y++ self.y++ yield delay(1, true, false) self.y++ self.y++ }) })) const x = X.create() const values: number[] = [] reaction( () => x.y, v => values.push(v) ) await x.p() expect(x.y).toBe(5) expect(values).toEqual([3, 5]) }) test("can handle async actions", () => { testCoffeeTodo( self => function* fetchData(kind: string) { self.title = "getting coffee " + kind self.title = yield delay(100, "drinking coffee") return "awake" }, false, "awake", ["getting coffee black", "drinking coffee"] ) }) test("can handle erroring actions", () => { testCoffeeTodo( self => function* fetchData(kind: string) { throw kind }, true, "black", [] ) }) test("can handle try catch", () => { testCoffeeTodo( self => function* fetchData(kind: string) { try { yield delay(10, "tea", true) return undefined } catch (e) { self.title = e return "biscuit" } }, false, "biscuit", ["tea"] ) }) test("empty sequence works", () => { testCoffeeTodo(() => function* fetchData(kind: string) {}, false, undefined, []) }) test("can handle throw from yielded promise works", () => { testCoffeeTodo( () => function* fetchData(kind: string) { yield delay(10, "x", true) }, true, "x", [] ) }) test("typings", async () => { const M = types.model({ title: types.string }).actions(self => { function* a(x: string) { yield delay(10, "x", false) self.title = "7" return 23 } // tslint:disable-next-line:no-shadowed-variable const b = flow(function* b(x: string) { yield delay(10, "x", false) self.title = "7" return 24 }) return { a: flow(a), b } }) const m1 = M.create({ title: "test " }) const resA = m1.a("z") const resB = m1.b("z") const [x1, x2] = await Promise.all([resA, resB]) expect(x1).toBe(23) expect(x2).toBe(24) }) test("typings", async () => { const M = types.model({ title: types.string }).actions(self => { function* a(x: string) { yield delay(10, "x", false) self.title = "7" return 23 } // tslint:disable-next-line:no-shadowed-variable const b = flow(function* b(x: string) { yield delay(10, "x", false) self.title = "7" return 24 }) return { a: flow(a), b } }) const m1 = M.create({ title: "test " }) const resA = m1.a("z") const resB = m1.b("z") const [x1, x2] = await Promise.all([resA, resB]) expect(x1).toBe(23) expect(x2).toBe(24) }) test("recordActions should only emit invocation", async () => { let calls = 0 const M = types .model({ title: types.string }) .actions(self => { function* a(x: string) { yield delay(10, "x", false) calls++ return 23 } return { a: flow(a) } }) const m1 = M.create({ title: "test " }) const recorder = recordActions(m1) await m1.a("x") recorder.stop() expect(recorder.actions).toEqual([ { args: ["x"], name: "a", path: "" } ]) expect(calls).toBe(1) recorder.replay(m1) await new Promise(resolve => setTimeout(resolve, 50)) expect(calls).toBe(2) }) test("can handle nested async actions", () => { // tslint:disable-next-line:no-shadowed-variable const uppercase = flow(function* uppercase(value: string) { const res = yield delay(20, value.toUpperCase()) return res }) testCoffeeTodo( self => function* fetchData(kind: string) { self.title = yield uppercase("drinking " + kind) return self.title }, false, "DRINKING BLACK", ["DRINKING BLACK"] ) }) test("can handle nested async actions when using decorate", async () => { const events: [IMiddlewareEventType, string][] = [] const middleware: IMiddlewareHandler = (call, next) => { events.push([call.type, call.name]) return next(call) } // tslint:disable-next-line:no-shadowed-variable const uppercase = flow(function* uppercase(value: string) { const res = yield delay(20, value.toUpperCase()) return res }) const Todo = types.model({}).actions(self => { // tslint:disable-next-line:no-shadowed-variable const act = flow(function* act(value: string) { return yield uppercase(value) }) return { act: decorate(middleware, act) } }) const res = await Todo.create().act("x") expect(res).toBe("X") expect(events).toEqual([ ["action", "act"], ["flow_spawn", "act"], ["flow_resume", "act"], ["flow_resume", "act"], ["flow_return", "act"] ]) }) test("flow gain back control when node become not alive during yield", async () => { expect.assertions(2) const rejectError = new Error("Reject Error") const MyModel = types.model({}).actions(() => { return { doAction() { return flow(function* () { try { yield delay(20, "").then(() => Promise.reject(rejectError)) } catch (e) { expect(e).toEqual(rejectError) throw e } })() } } }) const m = MyModel.create({}) const p = m.doAction() destroy(m) try { await p } catch (e) { expect(e).toEqual(rejectError) } }) function filterRelevantStuff(stuff: Partial>[]) { return stuff.map(x => { delete x.context delete x.tree return x }) } test("flow typings", async () => { const promise = Promise.resolve() const M = types.model({ x: 5 }).actions(self => ({ // should be () => Promise voidToVoid: flow(function* () { yield promise }), // should be (val: number) => Promise numberToNumber: flow(function* (val: number) { yield promise return val }), // should be () => Promise voidToNumber: flow(function* () { yield promise return Promise.resolve(2) }) })) const m = M.create() // these should compile const a: void = await m.voidToVoid() expect(a).toBe(undefined) const b: number = await m.numberToNumber(4) expect(b).toBe(4) const c: number = await m.voidToNumber() expect(c).toBe(2) await m.voidToNumber().then(d => { const _d: number = d expect(_d).toBe(2) }) }) /** * Detect explicit `any` type. * https://stackoverflow.com/a/55541672/4289902 */ type IfAny = 0 extends 1 & T ? Y : N /** * Ensure that the type of the passed value is of the expected type, and is NOT the TypeScript `any` type */ function ensureNotAnyType(value: IfAny) {} test("yield* typings for toGeneratorFunction", async () => { const voidPromise = () => Promise.resolve() const numberPromise = () => Promise.resolve(7) const stringWithArgsPromise = (input1: string, input2: boolean) => Promise.resolve("test-result") const voidGen = toGeneratorFunction(voidPromise) const numberGen = toGeneratorFunction(numberPromise) const stringWithArgsGen = toGeneratorFunction(stringWithArgsPromise) const M = types.model({ x: 5 }).actions(self => { function* testAction() { const voidResult = yield* voidGen() ensureNotAnyType(voidResult) const numberResult = yield* numberGen() ensureNotAnyType(numberResult) const stringResult = yield* stringWithArgsGen("input", true) ensureNotAnyType(stringResult) return stringResult } return { testAction: flow(testAction) } }) const m = M.create() const result = await m.testAction() ensureNotAnyType(result) expect(result).toBe("test-result") }) test("yield* typings for toGenerator", async () => { const voidPromise = () => Promise.resolve() const numberPromise = () => Promise.resolve(7) const stringWithArgsPromise = (input1: string, input2: boolean) => Promise.resolve("test-result") const M = types.model({ x: 5 }).actions(self => { function* testAction() { const voidResult = yield* toGenerator(voidPromise()) ensureNotAnyType(voidResult) const numberResult = yield* toGenerator(numberPromise()) ensureNotAnyType(numberResult) const stringResult = yield* toGenerator(stringWithArgsPromise("input", true)) ensureNotAnyType(stringResult) return stringResult } return { testAction: flow(testAction) } }) const m = M.create() const result = await m.testAction() ensureNotAnyType(result) expect(result).toBe("test-result") }) ================================================ FILE: __tests__/core/bigint.test.ts ================================================ import { t } from "../../src" import { Hook, NodeLifeCycle } from "../../src/internal" import { describe, it, expect, test } from "bun:test" describe("types.bigint", () => { describe("methods", () => { describe("create", () => { describe("with no arguments", () => { if (process.env.NODE_ENV !== "production") { it("should throw an error in development", () => { expect(() => { t.bigint.create() }).toThrow() }) } }) describe("with a bigint argument", () => { it("should return a bigint", () => { const n = t.bigint.create(BigInt(1)) expect(typeof n).toBe("bigint") }) }) describe("with a number argument", () => { it("should return a bigint", () => { const n = t.bigint.create(1) expect(typeof n).toBe("bigint") expect(n).toBe(BigInt(1)) }) }) describe("with a string argument", () => { it("should return a bigint", () => { const n = t.bigint.create("2") expect(typeof n).toBe("bigint") expect(n).toBe(BigInt(2)) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error() ] if (process.env.NODE_ENV !== "production") { testCases.forEach(testCase => { it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => { expect(() => { t.bigint.create(testCase as any) }).toThrow() }) }) } }) }) describe("describe", () => { it("should return the value 'bigint'", () => { const description = t.bigint.describe() expect(description).toBe("bigint") }) }) describe("getSnapshot", () => { it("should return the value as string (JSON-safe)", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) const snapshot = t.bigint.getSnapshot(n) expect(snapshot).toBe("1") expect(typeof snapshot).toBe("string") }) }) describe("getSubtype", () => { it("should return null", () => { const subtype = t.bigint.getSubTypes() expect(subtype).toBe(null) }) }) describe("instantiate", () => { if (process.env.NODE_ENV !== "production") { describe("with invalid arguments", () => { it("should throw when passed undefined", () => { expect(() => { t.bigint.instantiate(null, "", {}, undefined as any) }).toThrow() }) }) } describe("with a bigint argument", () => { it("should return an object", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) expect(typeof n).toBe("object") }) }) }) describe("is", () => { describe("with a bigint argument", () => { it("should return true", () => { const result = t.bigint.is(BigInt(1)) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error() ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.bigint.is(testCase as any) expect(result).toBe(false) }) }) }) describe("with a string argument", () => { it("should return true (string is valid snapshot input)", () => { expect(t.bigint.is("1")).toBe(true) }) }) describe("with a number argument", () => { it("should return true (number is valid snapshot input)", () => { expect(t.bigint.is(1)).toBe(true) }) }) }) describe("isAssignableFrom", () => { describe("with a bigint argument", () => { it("should return true", () => { const result = t.bigint.isAssignableFrom(t.bigint) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ t.Date, t.boolean, t.finite, t.float, t.identifier, t.identifierNumber, t.integer, t.null, t.string, t.undefined ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.bigint.isAssignableFrom(testCase as any) expect(result).toBe(false) }) }) }) }) describe("validate", () => { describe("with a bigint, string or number argument", () => { it("should return with no validation errors for bigint", () => { const result = t.bigint.validate(BigInt(1), []) expect(result).toEqual([]) }) it("should return with no validation errors for string", () => { const result = t.bigint.validate("1", []) expect(result).toEqual([]) }) it("should return with no validation errors for number", () => { const result = t.bigint.validate(1, []) expect(result).toEqual([]) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error() ] testCases.forEach(testCase => { it(`should return with a validation error when passed ${JSON.stringify( testCase )}`, () => { const result = t.bigint.validate(testCase as any, []) expect(result).toEqual([ { context: [], message: "Value is not a bigint", value: testCase } ]) }) }) }) }) }) describe("properties", () => { describe("flags", () => { test("return the correct value", () => { const flags = t.bigint.flags expect(flags).toBe(1 << 23) }) }) describe("identifierAttribute", () => { test("returns undefined", () => { const identifierAttribute = t.bigint.identifierAttribute expect(identifierAttribute).toBeUndefined() }) }) describe("isType", () => { test("returns true", () => { const isType = t.bigint.isType expect(isType).toBe(true) }) }) describe("name", () => { test('returns "bigint"', () => { const name = t.bigint.name expect(name).toBe("bigint") }) }) }) describe("instance", () => { describe("methods", () => { describe("aboutToDie", () => { it("calls the beforeDetach hook", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) let called = false n.registerHook(Hook.beforeDestroy, () => { called = true }) n.aboutToDie() expect(called).toBe(true) }) }) describe("die", () => { it("kills the node", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) n.die() expect(n.isAlive).toBe(false) }) it("should mark the node as dead", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) n.die() expect(n.state).toBe(NodeLifeCycle.DEAD) }) }) describe("finalizeCreation", () => { it("should mark the node as finalized", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) n.finalizeCreation() expect(n.state).toBe(NodeLifeCycle.FINALIZED) }) }) describe("finalizeDeath", () => { it("should mark the node as dead", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) n.finalizeDeath() expect(n.state).toBe(NodeLifeCycle.DEAD) }) }) describe("getReconciliationType", () => { it("should return the correct type", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) const type = n.getReconciliationType() expect(type).toBe(t.bigint) }) }) describe("getSnapshot", () => { it("should return the value as string (JSON-safe)", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) const snapshot = n.getSnapshot() expect(snapshot).toBe("1") expect(typeof snapshot).toBe("string") }) }) describe("registerHook", () => { it("should register a hook and call it", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) let called = false n.registerHook(Hook.beforeDestroy, () => { called = true }) n.die() expect(called).toBe(true) }) }) describe("setParent", () => { if (process.env.NODE_ENV !== "production") { describe("with null", () => { it("should throw an error", () => { const n = t.bigint.instantiate(null, "", {}, BigInt(1)) expect(() => { n.setParent(null, "foo") }).toThrow() }) }) describe("with a parent object", () => { it("should throw an error", () => { const Parent = t.model({ child: t.bigint }) const parent = Parent.create({ child: BigInt(1) }) const n = t.bigint.instantiate(null, "", {}, BigInt(1)) expect(() => { // @ts-ignore n.setParent(parent, "bar") }).toThrow( "[mobx-state-tree] assertion failed: scalar nodes cannot change their parent" ) }) }) } }) }) }) }) ================================================ FILE: __tests__/core/boolean.test.ts ================================================ import { t } from "../../src" import { Hook, NodeLifeCycle } from "../../src/internal" import { describe, expect, it, test } from "bun:test" describe("types.boolean", () => { describe("methods", () => { describe("create", () => { describe("with no arguments", () => { if (process.env.NODE_ENV !== "production") { it("should throw an error in development", () => { expect(() => { t.boolean.create() }).toThrow() }) } }) describe("with a boolean argument", () => { it("should return a boolean", () => { const n = t.boolean.create(true) expect(typeof n).toBe("boolean") }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, "string", 1, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error(), Infinity, NaN ] if (process.env.NODE_ENV !== "production") { testCases.forEach(testCase => { it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => { expect(() => { t.boolean.create(testCase as any) }).toThrow() }) }) } }) }) describe("describe", () => { it("should return the value 'boolean'", () => { const description = t.boolean.describe() expect(description).toBe("boolean") }) }) describe("getSnapshot", () => { it("should return the value passed in", () => { const b = t.boolean.instantiate(null, "", {}, true) const snapshot = t.boolean.getSnapshot(b) expect(snapshot).toBe(true) }) }) describe("getSubtype", () => { it("should return null", () => { const subtype = t.boolean.getSubTypes() expect(subtype).toBe(null) }) }) describe("instantiate", () => { if (process.env.NODE_ENV !== "production") { describe("with invalid arguments", () => { it("should not throw an error", () => { expect(() => { // @ts-ignore t.boolean.instantiate() }).not.toThrow() }) }) } describe("with a boolean argument", () => { it("should return an object", () => { const b = t.boolean.instantiate(null, "", {}, true) expect(typeof b).toBe("object") }) }) }) describe("is", () => { describe("with a boolean argument", () => { it("should return true", () => { const result = t.boolean.is(true) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, "string", 1, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error(), Infinity, NaN ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.boolean.is(testCase as any) expect(result).toBe(false) }) }) }) }) describe("isAssignableFrom", () => { describe("with a boolean argument", () => { it("should return true", () => { const result = t.boolean.isAssignableFrom(t.boolean) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ t.Date, t.number, t.finite, t.float, t.identifier, t.identifierNumber, t.integer, t.null, t.string, t.undefined ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.boolean.isAssignableFrom(testCase as any) expect(result).toBe(false) }) }) }) }) // TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single boolean nodes. describe.skip("reconcile", () => {}) describe("validate", () => { describe("with a boolean argument", () => { it("should return with no validation errors", () => { const result = t.boolean.validate(true, []) expect(result).toEqual([]) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, "string", 1, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error(), Infinity, NaN ] testCases.forEach(testCase => { it(`should return with a validation error when passed ${JSON.stringify( testCase )}`, () => { const result = t.boolean.validate(testCase as any, []) expect(result).toEqual([ { context: [], message: "Value is not a boolean", value: testCase } ]) }) }) }) }) }) describe("properties", () => { describe("flags", () => { test("return the correct value", () => { const flags = t.boolean.flags expect(flags).toBe(4) }) }) describe("identifierAttribute", () => { // We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined. test("returns undefined", () => { const identifierAttribute = t.boolean.identifierAttribute expect(identifierAttribute).toBeUndefined() }) }) describe("isType", () => { test("returns true", () => { const isType = t.boolean.isType expect(isType).toBe(true) }) }) describe("name", () => { test('returns "boolean"', () => { const name = t.boolean.name expect(name).toBe("boolean") }) }) }) describe("instance", () => { describe("methods", () => { describe("aboutToDie", () => { it("calls the beforeDetach hook", () => { const b = t.boolean.instantiate(null, "", {}, true) let called = false b.registerHook(Hook.beforeDestroy, () => { called = true }) b.aboutToDie() expect(called).toBe(true) }) }) describe("die", () => { it("kills the node", () => { const b = t.boolean.instantiate(null, "", {}, true) b.die() expect(b.isAlive).toBe(false) }) it("should mark the node as dead", () => { const b = t.boolean.instantiate(null, "", {}, true) b.die() expect(b.state).toBe(NodeLifeCycle.DEAD) }) }) describe("finalizeCreation", () => { it("should mark the node as finalized", () => { const b = t.boolean.instantiate(null, "", {}, true) b.finalizeCreation() expect(b.state).toBe(NodeLifeCycle.FINALIZED) }) }) describe("finalizeDeath", () => { it("should mark the node as dead", () => { const b = t.boolean.instantiate(null, "", {}, true) b.finalizeDeath() expect(b.state).toBe(NodeLifeCycle.DEAD) }) }) describe("getReconciliationType", () => { it("should return the correct type", () => { const b = t.boolean.instantiate(null, "", {}, true) const type = b.getReconciliationType() expect(type).toBe(t.boolean) }) }) describe("getSnapshot", () => { it("should return the value passed in", () => { const b = t.boolean.instantiate(null, "", {}, true) const snapshot = b.getSnapshot() expect(snapshot).toBe(true) }) }) describe("registerHook", () => { it("should register a hook and call it", () => { const b = t.boolean.instantiate(null, "", {}, true) let called = false b.registerHook(Hook.beforeDestroy, () => { called = true }) b.die() expect(called).toBe(true) }) }) describe("setParent", () => { if (process.env.NODE_ENV !== "production") { describe("with null", () => { it("should throw an error", () => { const b = t.boolean.instantiate(null, "", {}, true) expect(() => { b.setParent(null, "foo") }).toThrow() }) }) describe("with a parent object", () => { it("should throw an error", () => { const Parent = t.model({ child: t.boolean }) const parent = Parent.create({ child: true }) const b = t.boolean.instantiate(null, "", {}, true) expect(() => { // @ts-ignore b.setParent(parent, "bar") }).toThrow( "[mobx-state-tree] assertion failed: scalar nodes cannot change their parent" ) }) }) } }) }) }) }) ================================================ FILE: __tests__/core/boxes-store.test.ts ================================================ import { values } from "mobx" import { types, getParent, hasParent, recordPatches, unprotect, getSnapshot, Instance } from "../../src" import { expect, test } from "bun:test" export const Box = types .model("Box", { id: types.identifier, name: "", x: 0, y: 0 }) .views(self => ({ get width() { return self.name.length * 15 }, get isSelected(): boolean { if (!hasParent(self)) return false return getParent(getParent(self)).selection === self } })) .actions(self => { function move(dx: number, dy: number) { self.x += dx self.y += dy } function setName(newName: string) { self.name = newName } return { move, setName } }) export const Arrow = types.model("Arrow", { id: types.identifier, from: types.reference(Box), to: types.reference(Box) }) export const Store = types .model("Store", { boxes: types.map(Box), arrows: types.array(Arrow), selection: types.reference(Box) }) .actions(self => { function afterCreate() { unprotect(self) } function addBox(id: string, name: string, x: number, y: number) { const box = Box.create({ name, x, y, id }) self.boxes.put(box) return box } function addArrow(id: string, from: string, to: string) { self.arrows.push(Arrow.create({ id, from, to })) } function setSelection(selection: Instance) { self.selection = selection } function createBox( id: string, name: string, x: number, y: number, source: Instance | null | undefined, arrowId: string | null ) { const box = addBox(id, name, x, y) setSelection(box) if (source) addArrow(arrowId!, source.id, box.id) } return { afterCreate, addBox, addArrow, setSelection, createBox } }) function createStore() { return Store.create({ boxes: { cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 }, aa: { id: "aa", name: "Bratislava", x: 650, y: 300 } }, arrows: [{ id: "dd", from: "cc", to: "aa" }], selection: "aa" }) } test("store is deserialized correctly", () => { const s = createStore() expect(s.boxes.size).toBe(2) expect(s.arrows.length).toBe(1) expect(s.selection === s.boxes.get("aa")).toBe(true) expect(s.arrows[0].from.name).toBe("Rotterdam") expect(s.arrows[0].to.name).toBe("Bratislava") expect(values(s.boxes).map(b => b.isSelected)).toEqual([false, true]) }) test("store emits correct patch paths", () => { const s = createStore() const recorder1 = recordPatches(s) const recorder2 = recordPatches(s.boxes) const recorder3 = recordPatches(s.boxes.get("cc")!) s.arrows[0].from.x += 117 expect(recorder1.patches).toEqual([{ op: "replace", path: "/boxes/cc/x", value: 217 }]) expect(recorder2.patches).toEqual([{ op: "replace", path: "/cc/x", value: 217 }]) expect(recorder3.patches).toEqual([{ op: "replace", path: "/x", value: 217 }]) }) test("box operations works correctly", () => { const s = createStore() s.createBox("a", "A", 0, 0, null, null) s.createBox("b", "B", 100, 100, s.boxes.get("aa"), "aa2b") expect(getSnapshot(s)).toEqual({ boxes: { cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 }, aa: { id: "aa", name: "Bratislava", x: 650, y: 300 }, a: { id: "a", name: "A", x: 0, y: 0 }, b: { id: "b", name: "B", x: 100, y: 100 } }, arrows: [ { id: "dd", from: "cc", to: "aa" }, { id: "aa2b", from: "aa", to: "b" } ], selection: "b" }) s.boxes.get("a")!.setName("I'm groot") expect(getSnapshot(s)).toEqual({ boxes: { cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 }, aa: { id: "aa", name: "Bratislava", x: 650, y: 300 }, a: { id: "a", name: "I'm groot", x: 0, y: 0 }, b: { id: "b", name: "B", x: 100, y: 100 } }, arrows: [ { id: "dd", from: "cc", to: "aa" }, { id: "aa2b", from: "aa", to: "b" } ], selection: "b" }) expect(JSON.stringify(s)).toEqual(JSON.stringify(getSnapshot(s))) s.boxes.get("a")!.move(50, 50) expect(getSnapshot(s)).toEqual({ boxes: { cc: { id: "cc", name: "Rotterdam", x: 100, y: 100 }, aa: { id: "aa", name: "Bratislava", x: 650, y: 300 }, a: { id: "a", name: "I'm groot", x: 50, y: 50 }, b: { id: "b", name: "B", x: 100, y: 100 } }, arrows: [ { id: "dd", from: "cc", to: "aa" }, { id: "aa2b", from: "aa", to: "b" } ], selection: "b" }) expect(s.boxes.get("b")!.width).toBe(15) expect(Box.create({ id: "hello" }).isSelected).toBe(false) }) ================================================ FILE: __tests__/core/circular1.test.ts ================================================ import { types } from "../../src" import { LateTodo2, LateStore2 } from "./circular2.test" import { expect, test } from "bun:test" // combine function hosting with types.late to support circular refs between files! export function LateStore1() { return types.model({ todo: types.late(LateTodo2) }) } export function LateTodo1() { return types.model({ done: types.boolean }) } test("circular test 1 should work", () => { const Store1 = types.late(LateStore1) const Store2 = types.late(LateStore2) expect(Store1.is({})).toBe(false) expect(Store1.is({ todo: { done: true } })).toBe(true) const s1 = Store1.create({ todo: { done: true } }) expect(s1.todo.done).toBe(true) expect(Store2.is({})).toBe(false) expect(Store2.is({ todo: { done: true } })).toBe(true) const s2 = Store2.create({ todo: { done: true } }) expect(s2.todo.done).toBe(true) }) ================================================ FILE: __tests__/core/circular2.test.ts ================================================ import { types } from "../../src" import { LateTodo1, LateStore1 } from "./circular1.test" import { expect, test } from "bun:test" // combine function hosting with types.late to support circular refs between files! export function LateTodo2() { return types.model({ done: types.boolean }) } export function LateStore2() { return types.model({ todo: types.late(LateTodo1) }) } test("circular test 2 should work", () => { const Store1 = types.late(LateStore1) const Store2 = types.late(LateStore2) expect(Store1.is({})).toBe(false) expect(Store1.is({ todo: { done: true } })).toBe(true) const s1 = Store1.create({ todo: { done: true } }) expect(s1.todo.done).toBe(true) expect(Store2.is({})).toBe(false) expect(Store2.is({ todo: { done: true } })).toBe(true) const s2 = Store2.create({ todo: { done: true } }) expect(s2.todo.done).toBe(true) }) ================================================ FILE: __tests__/core/custom-type.test.ts ================================================ import { types, recordPatches, onSnapshot, unprotect, applySnapshot, applyPatch, SnapshotOut } from "../../src" import { expect, jest, test } from "bun:test" class Decimal { public number: number public fraction: number constructor(value: string) { const parts = value.split(".") this.number = Number(parts[0]) this.fraction = Number(parts[1]) } toNumber() { return this.number + Number("0." + this.fraction) } toString() { return `${this.number}.${this.fraction}` } } { const DecimalPrimitive = types.custom({ name: "Decimal", fromSnapshot(value: string, env: any) { if (env && env.test) env.test(value) return new Decimal(value) }, toSnapshot(value: Decimal) { return value.toString() }, isTargetType(value: string | Decimal): value is Decimal { return value instanceof Decimal }, getValidationMessage(value: string): string { if (/^-?\d+\.\d+$/.test(value)) return "" // OK return `'${value}' doesn't look like a valid decimal number` } }) const Wallet = types.model({ balance: DecimalPrimitive, lastTransaction: types.maybeNull(DecimalPrimitive) }) test("it should allow for custom primitive types", () => { const w1 = Wallet.create({ balance: new Decimal("2.5") }) expect(w1.balance.number).toBe(2) expect(w1.balance.fraction).toBe(5) const w2 = Wallet.create({ balance: "3.5" }) expect(w2.balance.number).toBe(3) expect(w2.balance.fraction).toBe(5) if (process.env.NODE_ENV !== "production") expect(() => Wallet.create({ balance: "two point one" })).toThrow( "(Invalid value for type 'Decimal': 'two point one' doesn't look like a valid decimal number)" ) }) // test reassignment / reconcilation / conversion works test("reassignments will work", () => { const w1 = Wallet.create({ balance: "2.5" }) unprotect(w1) const p = recordPatches(w1) const snapshots: SnapshotOut[] = [] onSnapshot(w1, s => { snapshots.push(s) }) const b1 = w1.balance expect(b1).toBeInstanceOf(Decimal) w1.balance = "2.5" as any // TODO: make cast work with custom types expect(b1).toBeInstanceOf(Decimal) expect(w1.balance).toBe(b1) // reconciled w1.balance = new Decimal("2.5") // not reconciling! // TODO: introduce custom hook for that? expect(b1).toBeInstanceOf(Decimal) w1.balance = new Decimal("3.5") expect(b1).toBeInstanceOf(Decimal) w1.balance = "4.5" as any expect(b1).toBeInstanceOf(Decimal) w1.lastTransaction = b1 expect(w1.lastTransaction).toBe(b1) w1.lastTransaction = null expect(w1.lastTransaction).toBe(null) // patches & snapshots expect(snapshots).toMatchSnapshot() p.stop() expect(p.patches).toMatchSnapshot() }) test("passes environment to fromSnapshot", () => { const env = { test: jest.fn() } Wallet.create({ balance: "3.0" }, env) expect(env.test).toHaveBeenCalledWith("3.0") }) } { test("complex representation", () => {}) const DecimalTuple = types.custom<[number, number], Decimal>({ name: "DecimalTuple", fromSnapshot(value: [number, number]) { return new Decimal(value[0] + "." + value[1]) }, toSnapshot(value: Decimal) { return [value.number, value.fraction] }, isTargetType(value: [number, number] | Decimal): value is Decimal { return value instanceof Decimal }, getValidationMessage(value: [number, number]): string { if (Array.isArray(value) && value.length === 2) return "" // OK return `'${JSON.stringify(value)}' doesn't look like a valid decimal number` } }) const Wallet = types.model({ balance: DecimalTuple }) test("it should allow for complex custom primitive types", () => { const w1 = Wallet.create({ balance: new Decimal("2.5") }) expect(w1.balance.number).toBe(2) expect(w1.balance.fraction).toBe(5) const w2 = Wallet.create({ balance: [3, 5] }) expect(w2.balance.number).toBe(3) expect(w2.balance.fraction).toBe(5) if (process.env.NODE_ENV !== "production") expect(() => Wallet.create({ balance: "two point one" } as any)).toThrow( "(Invalid value for type 'DecimalTuple': '\"two point one\"' doesn't look like a valid decimal number)" ) }) // test reassignment / reconcilation / conversion works test("complex reassignments will work", () => { const w1 = Wallet.create({ balance: [2, 5] }) unprotect(w1) const p = recordPatches(w1) const snapshots: SnapshotOut[] = [] onSnapshot(w1, s => { snapshots.push(s) }) const b1 = w1.balance expect(b1).toBeInstanceOf(Decimal) w1.balance = [2, 5] as any expect(b1).toBeInstanceOf(Decimal) expect(w1.balance).not.toBe(b1) // not reconciled, balance is not deep equaled (TODO: future feature?) w1.balance = new Decimal("2.5") // not reconciling! expect(b1).toBeInstanceOf(Decimal) w1.balance = new Decimal("3.5") expect(b1).toBeInstanceOf(Decimal) w1.balance = [4, 5] as any expect(b1).toBeInstanceOf(Decimal) // patches & snapshots expect(snapshots).toMatchSnapshot() p.stop() expect(p.patches).toMatchSnapshot() }) test("can apply snapshot and patch", () => { const w1 = Wallet.create({ balance: [3, 0] }) applySnapshot(w1, { balance: [4, 5] }) expect(w1.balance).toBeInstanceOf(Decimal) expect(w1.balance.toString()).toBe("4.5") applyPatch(w1, { op: "replace", path: "/balance", value: [5, 0] }) expect(w1.balance.toString()).toBe("5.0") }) } ================================================ FILE: __tests__/core/date.test.ts ================================================ import { t } from "../../src" import { Hook, NodeLifeCycle } from "../../src/internal" import { describe, expect, it, test } from "bun:test" describe("types.date", () => { describe("methods", () => { describe("create", () => { describe("with no arguments", () => { if (process.env.NODE_ENV !== "production") { it("should throw an error in development", () => { expect(() => { t.Date.create() }).toThrow() }) } }) describe("with a number argument", () => { it("should return a Date object", () => { const d = t.Date.create(1701369873059) expect(d instanceof Date).toBe(true) }) }) describe("with a Date argument", () => { it("should return a Date object", () => { const input = new Date() const d = t.Date.create(input) expect(d instanceof Date).toBe(true) }) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, true, [], function () {}, "2022-01-01T00:00:00.000Z", /a/, new Map(), new Set(), Symbol(), new Error() ] if (process.env.NODE_ENV !== "production") { testCases.forEach(testCase => { it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => { expect(() => { t.Date.create(testCase as any) }).toThrow() }) }) } }) }) describe("describe", () => { it("should return the value 'Date'", () => { const description = t.Date.describe() expect(description).toBe("Date") }) }) describe("getSnapshot", () => { it("should return a number from a date", () => { const date = new Date("2022-01-01T00:00:00.000Z") const d = t.Date.instantiate(null, "", {}, date) const snapshot = t.Date.getSnapshot(d) expect(snapshot).toBe(1640995200000) }) it("should return a number from a number", () => { const d = t.Date.instantiate(null, "", {}, 1701369873059) const snapshot = t.Date.getSnapshot(d) expect(snapshot).toBe(1701369873059) }) }) describe("getSubtype", () => { it("should return null", () => { const subtype = t.Date.getSubTypes() expect(subtype).toBe(null) }) }) describe("instantiate", () => { if (process.env.NODE_ENV !== "production") { describe("with invalid arguments", () => { it("should not throw an error", () => { expect(() => { // @ts-ignore t.Date.instantiate() }).not.toThrow() }) }) } describe("with a Date argument", () => { it("should return an object", () => { const s = t.Date.instantiate(null, "", {}, new Date()) expect(typeof s).toBe("object") }) }) describe("with a number argument", () => { it("should return an object", () => { const s = t.Date.instantiate(null, "", {}, 1701369873059) expect(typeof s).toBe("object") }) }) }) describe("is", () => { describe("with a Date argument", () => { it("should return true", () => { const result = t.Date.is(new Date()) expect(result).toBe(true) }) }) describe("with a number argument", () => { it("should return true", () => { const result = t.Date.is(1701369873059) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, true, [], function () {}, "2022-01-01T00:00:00.000Z", /a/, new Map(), new Set(), Symbol(), new Error() ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.Date.is(testCase as any) expect(result).toBe(false) }) }) }) }) describe("isAssignableFrom", () => { describe("with a Date argument", () => { it("should return true", () => { const result = t.Date.isAssignableFrom(t.Date) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ t.string, t.boolean, t.finite, t.float, t.identifier, t.identifierNumber, t.integer, t.null, t.number, t.undefined ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.Date.isAssignableFrom(testCase as any) expect(result).toBe(false) }) }) }) // TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single date nodes. describe.skip("reconcile", () => {}) describe("validate", () => { describe("with a Date argument", () => { it("should return with no validation errors", () => { const result = t.Date.validate(new Date(), []) expect(result).toEqual([]) }) }) describe("with a number argument", () => { it("should return with no validation errors", () => { const result = t.Date.validate(1701369873059, []) expect(result).toEqual([]) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, "2022-01-01T00:00:00.000Z", true, [], function () {}, /a/, new Map(), new Set(), Symbol(), new Error() ] testCases.forEach(testCase => { it(`should return with a validation error when passed ${JSON.stringify( testCase )}`, () => { const result = t.Date.validate(testCase as any, []) expect(result).toEqual([ { context: [], message: "Value is not a Date or a unix milliseconds timestamp", value: testCase } ]) }) }) }) }) }) describe("properties", () => { describe("flags", () => { test("return the correct value", () => { const flags = t.Date.flags expect(flags).toBe(8) }) }) describe("identifierAttribute", () => { // We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined. test("returns undefined", () => { const identifierAttribute = t.Date.identifierAttribute expect(identifierAttribute).toBeUndefined() }) }) describe("isType", () => { test("returns true", () => { const isType = t.Date.isType expect(isType).toBe(true) }) }) describe("name", () => { test('returns "Date"', () => { const name = t.Date.name expect(name).toBe("Date") }) }) }) describe("instance", () => { describe("methods", () => { describe("aboutToDie", () => { it("calls the beforeDetach hook", () => { const d = t.Date.instantiate(null, "", {}, new Date()) let called = false d.registerHook(Hook.beforeDestroy, () => { called = true }) d.aboutToDie() expect(called).toBe(true) }) }) describe("die", () => { it("kills the node", () => { const d = t.Date.instantiate(null, "", {}, new Date()) d.die() expect(d.isAlive).toBe(false) }) it("should mark the node as dead", () => { const d = t.Date.instantiate(null, "", {}, new Date()) d.die() expect(d.state).toBe(NodeLifeCycle.DEAD) }) }) describe("finalizeCreation", () => { it("should mark the node as finalized", () => { const d = t.Date.instantiate(null, "", {}, new Date()) d.finalizeCreation() expect(d.state).toBe(NodeLifeCycle.FINALIZED) }) }) describe("finalizeDeath", () => { it("should mark the node as dead", () => { const d = t.Date.instantiate(null, "", {}, new Date()) d.finalizeDeath() expect(d.state).toBe(NodeLifeCycle.DEAD) }) }) describe("getReconciliationType", () => { it("should return the correct type", () => { const d = t.Date.instantiate(null, "", {}, new Date()) const type = d.getReconciliationType() expect(type).toBe(t.Date) }) }) describe("getSnapshot", () => { it("should return the value passed in with a number", () => { const d = t.Date.instantiate(null, "", {}, 1701373349399) const snapshot = d.getSnapshot() expect(snapshot).toBe(1701373349399) }) it("should return the correct date getTime value with a date object", () => { const date = new Date("2022-01-01T00:00:00.000Z") const d = t.Date.instantiate(null, "", {}, date) const snapshot = d.getSnapshot() const expected = date.getTime() expect(snapshot).toBe(expected) }) }) describe("registerHook", () => { it("should register a hook and call it", () => { const d = t.Date.instantiate(null, "", {}, new Date()) let called = false d.registerHook(Hook.beforeDestroy, () => { called = true }) d.die() expect(called).toBe(true) }) }) describe("setParent", () => { if (process.env.NODE_ENV !== "production") { describe("with null", () => { it("should throw an error", () => { const d = t.Date.instantiate(null, "", {}, new Date()) expect(() => { d.setParent(null, "foo") }).toThrow() }) }) describe("with a parent object", () => { it("should throw an error", () => { const Parent = t.model({ child: t.Date }) const parent = Parent.create({ child: new Date() }) const d = t.Date.instantiate(null, "", {}, new Date()) expect(() => { // @ts-ignore d.setParent(parent, "bar") }).toThrow( "[mobx-state-tree] assertion failed: scalar nodes cannot change their parent" ) }) }) } }) }) }) }) ================================================ FILE: __tests__/core/deprecated.test.ts ================================================ import { deprecated } from "../../src/utils" import { flow, createFlowSpawner } from "../../src/core/flow" import { process as mstProcess, createProcessSpawner } from "../../src/core/process" import { expect, jest, test } from "bun:test" function createDeprecationListener() { // clear previous deprecation dedupe keys deprecated.ids = {} // save console.warn native implementation const originalWarn = console.warn // create spy to track warning call const spyWarn = (console.warn = jest.fn()) // return callback to check if warn was called properly return function isDeprecated() { // replace original implementation console.warn = originalWarn // test for correct log message, if in development if (process.env.NODE_ENV !== "production") { expect(spyWarn).toHaveBeenCalledTimes(1) expect(spyWarn.mock.calls[0][0].message).toMatch(/Deprecation warning:/) } } } test("`process` should mirror `flow`", () => { const isDeprecated = createDeprecationListener() const generator = function* () {} const flowResult = flow(generator) const processResult = mstProcess(generator) expect(processResult.name).toBe(flowResult.name) isDeprecated() }) test("`createProcessSpawner` should mirror `createFlowSpawner`", () => { const isDeprecated = createDeprecationListener() const alias = "generatorAlias" const generator = function* (): IterableIterator {} const flowSpawnerResult = createFlowSpawner(alias, generator) const processSpawnerResult = createProcessSpawner(alias, generator) expect(processSpawnerResult.name).toBe(flowSpawnerResult.name) isDeprecated() }) ================================================ FILE: __tests__/core/enum.test.ts ================================================ import { types, unprotect } from "../../src" import { expect, test } from "bun:test" enum ColorEnum { Red = "Red", Orange = "Orange", Green = "Green" } const colorEnumValues = Object.values(ColorEnum) as ColorEnum[] test("should support enums", () => { const TrafficLight = types.model({ color: types.enumeration("Color", colorEnumValues) }) expect(TrafficLight.is({ color: ColorEnum.Green })).toBe(true) expect(TrafficLight.is({ color: "Blue" })).toBe(false) expect(TrafficLight.is({ color: undefined })).toBe(false) const l = TrafficLight.create({ color: ColorEnum.Orange }) unprotect(l) l.color = ColorEnum.Red expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }') if (process.env.NODE_ENV !== "production") { expect(() => (l.color = "Blue" as any)).toThrow( /Error while converting `"Blue"` to `Color`/ ) } }) test("should support anonymous enums", () => { const TrafficLight = types.model({ color: types.enumeration(colorEnumValues) }) const l = TrafficLight.create({ color: ColorEnum.Orange }) unprotect(l) l.color = ColorEnum.Red expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }') if (process.env.NODE_ENV !== "production") { expect(() => (l.color = "Blue" as any)).toThrow( /Error while converting `"Blue"` to `"Red" | "Orange" | "Green"`/ ) } }) test("should support optional enums", () => { const TrafficLight = types.optional(types.enumeration(colorEnumValues), ColorEnum.Orange) const l = TrafficLight.create() expect(l).toBe(ColorEnum.Orange) }) test("should support optional enums inside a model", () => { const TrafficLight = types.model({ color: types.optional(types.enumeration(colorEnumValues), ColorEnum.Orange) }) const l = TrafficLight.create({}) expect(l.color).toBe(ColorEnum.Orange) }) test("should support plain string[] arrays", () => { const colorOptions: string[] = ["Red", "Orange", "Green"] const TrafficLight = types.model({ color: types.enumeration(colorOptions) }) const l = TrafficLight.create({ color: "Orange" }) unprotect(l) l.color = "Red" expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }') if (process.env.NODE_ENV !== "production") { expect(() => (l.color = "Blue" as any)).toThrow( /Error while converting `"Blue"` to `"Red" | "Orange" | "Green"`/ ) } }) test("should support readonly enums as const", () => { const colorOptions = ["Red", "Orange", "Green"] as const const TrafficLight = types.model({ color: types.enumeration(colorOptions) }) const l = TrafficLight.create({ color: "Orange" }) unprotect(l) l.color = "Red" expect(TrafficLight.describe()).toBe('{ color: ("Red" | "Orange" | "Green") }') if (process.env.NODE_ENV !== "production") { expect(() => (l.color = "Blue" as any)).toThrow( /Error while converting `"Blue"` to `"Red" | "Orange" | "Green"`/ ) } }) ================================================ FILE: __tests__/core/env.test.ts ================================================ import { configure } from "mobx" import { types, hasEnv, getEnv, clone, detach, unprotect, walk, getPath, castToSnapshot, hasParent, Instance, destroy, getParent, IAnyStateTreeNode, isStateTreeNode, isAlive } from "../../src" import { expect, test } from "bun:test" // tslint:disable: no-unused-expression const Todo = types .model({ title: "test" }) .views(self => ({ get description() { return getEnv(self).useUppercase ? self.title.toUpperCase() : self.title } })) const Store = types.model({ todos: types.array(Todo) }) function createEnvironment() { return { useUppercase: true } } test("it should be possible to use environments", () => { const env = createEnvironment() const todo = Todo.create({}, env) expect(hasEnv(todo)).toBe(true) expect(todo.description).toBe("TEST") env.useUppercase = false expect(todo.description).toBe("test") }) test("it should be possible to inherit environments", () => { const env = createEnvironment() const store = Store.create({ todos: [{}] }, env) expect(hasEnv(store.todos[0])).toBe(true) expect(store.todos[0].description).toBe("TEST") env.useUppercase = false expect(store.todos[0].description).toBe("test") }) test("getEnv throws error without environment", () => { const todo = Todo.create() expect(hasEnv(todo)).toBe(false) expect(() => getEnv(todo)).toThrow("Failed to find the environment of AnonymousModel@") }) test("detach should preserve environment", () => { const env = createEnvironment() const store = Store.create({ todos: [{}] }, env) unprotect(store) const todo = detach(store.todos[0]) expect(hasEnv(todo)).toBe(true) expect(todo.description).toBe("TEST") env.useUppercase = false expect(todo.description).toBe("test") }) test("it is possible to assign instance with the same environment as the parent to a tree", () => { const env = createEnvironment() const store = Store.create({ todos: [] }, env) const todo = Todo.create({}, env) unprotect(store) store.todos.push(todo) expect(store.todos.length === 1).toBe(true) expect(getEnv(store.todos) === getEnv(store.todos[0])).toBe(true) expect(getEnv(todo) === getEnv(store.todos[0])).toBe(true) }) test("it is not possible to assign instance with a different environment than the parent to a tree", () => { if (process.env.NODE_ENV !== "production") { const env1 = createEnvironment() const env2 = createEnvironment() const store = Store.create({ todos: [] }, env1) const todo = Todo.create({}, env2) unprotect(store) expect(() => store.todos.push(todo)).toThrow( "[mobx-state-tree] A state tree cannot be made part of another state tree as long as their environments are different." ) } }) test("it is possible to set a value inside a map of a map when using the same environment", () => { const env = createEnvironment() const EmptyModel = types.model({}) const MapOfEmptyModel = types.model({ map: types.map(EmptyModel) }) const MapOfMapOfEmptyModel = types.model({ map: types.map(MapOfEmptyModel) }) const mapOfMap = MapOfMapOfEmptyModel.create( { map: { whatever: { map: {} } } }, env ) unprotect(mapOfMap) // this should not throw mapOfMap.map.get("whatever")!.map.set("1234", EmptyModel.create({}, env)) expect(getEnv(mapOfMap) === env).toBe(true) expect(getEnv(mapOfMap.map.get("whatever")!.map.get("1234")!) === env).toBe(true) }) test("clone preserves environment", () => { const env = createEnvironment() const store = Store.create({ todos: [{}] }, env) { const todo = clone(store.todos[0]) expect(getEnv(todo) === env).toBe(true) } { const todo = clone(store.todos[0], true) expect(getEnv(todo) === env).toBe(true) expect(getEnv(todo) === env).toBe(true) } { const todo = clone(store.todos[0], false) expect(hasEnv(todo)).toBe(false) expect(() => { getEnv(todo) }).toThrow("Failed to find the environment of AnonymousModel@") } { const env2 = createEnvironment() const todo = clone(store.todos[0], env2) expect(env2 === getEnv(todo)).toBe(true) } }) test("#1231", () => { configure({ useProxies: "never" }) const envObj = createEnvironment() const logs: string[] = [] function nofParents(node: IAnyStateTreeNode) { let parents = 0 let parent = node while (hasParent(parent)) { parents++ parent = getParent(parent) } return parents } function leafsFirst(root: IAnyStateTreeNode) { const nodes: IAnyStateTreeNode[] = [] walk(root, i => { if (isStateTreeNode(i)) { nodes.push(i) } }) // sort by number of parents nodes.sort((a, b) => { return nofParents(b) - nofParents(a) }) return nodes } function check(root: Instance, name: string, mode: "detach" | "destroy") { function logFail(operation: string, n: any) { logs.push(`fail: (${name}) ${operation}: ${getPath(n)}, ${n}`) } function log(operation: string, n: any) { logs.push(`ok: (${name}) ${operation}: ${getPath(n)}, ${n}`) } // make sure all nodes are there root.s1.arr[0].title root.s1.m.get("one")!.title root.s2 const nodes = leafsFirst(root) expect(nodes.length).toBe(7) nodes.forEach(i => { const env = getEnv(i) const parent = hasParent(i) if (!parent && i !== root) { logFail("expected a parent, but none found", i) } else { log("had parent or was root", i) } if (env !== envObj) { logFail("expected same env as root, but was different", i) } else { log("same env as root", i) } }) unprotect(root) nodes.forEach(i => { const optional = optionalPaths.includes(getPath(i)) if (mode === "detach") { log("detaching node", i) detach(i) } else { log("destroying node", i) destroy(i) } const env = hasEnv(i) ? getEnv(i) : undefined const parent = hasParent(i) const alive = isAlive(i) if (mode === "detach") { if (parent) { logFail(`expected no parent after detach, but one was found`, i) } else { log(`no parent after detach`, i) } if (env !== envObj) { logFail("expected same env as root after detach, but it was not", i) } else { log("env kept after detach", i) } if (!alive) { logFail("expected to be alive after detach, but it was not", i) } else { log("alive after detach", i) } } else { // destroy might or might not keep the env, but doesn't matter so we don't check if (optional) { // optional (undefined) nodes will be assigned undefined and reconciled, therefore they will be kept alive if (!parent) { logFail( `expected a parent after destroy (since it is optional), but none was found`, i ) } else { log(`had parent after destroy (since it is optional)`, i) } if (!alive) { logFail( "expected to be alive after destroy (since it is optional), but it was not", i ) } else { log("alive after destroy (since it is optional)", i) } } else { if (parent) { logFail(`expected no parent after destroy, but one was found`, i) } else { log(`no parent after destroy`, i) } if (alive) { logFail("expected to be dead after destroy, but it was not", i) } else { log("dead after destroy", i) } } } }) } const T = types.model("T", { title: "some title" }) const S1Arr = types.array(T) const S1Map = types.map(T) const S1 = types.model("S1", { arr: S1Arr, m: S1Map }) const S2 = types.model("S2", {}) const RS = types.model("RS", { s1: types.optional(S1, {}), s2: types.optional(S2, {}) }) const optionalPaths = ["/s1", "/s2", "/s1/m", "/s1/arr"] const data = { s1: castToSnapshot( S1.create({ arr: S1Arr.create([T.create({})]), m: castToSnapshot(S1Map.create({ one: T.create({}) })) }) ), s2: S2.create() } const rsCreate = RS.create(data, envObj) const rsCreate2 = clone(rsCreate, true) const rsSnap = RS.create( { s1: { arr: [{}], m: { one: {} } }, s2: {} }, envObj ) const rsSnap2 = clone(rsCreate, true) check(rsCreate, "using create", "detach") check(rsSnap, "using snapshot", "detach") check(rsCreate2, "using create", "destroy") check(rsSnap2, "using snapshot", "destroy") const fails = logs.filter(l => l.startsWith("fail:")) if (fails.length > 0) { expect().fail(`\n${fails.join("\n")}`) } }) ================================================ FILE: __tests__/core/frozen.test.ts ================================================ import { getSnapshot, types, unprotect } from "../../src" import { expect, test } from "bun:test" test("it should accept any serializable value", () => { const Factory = types.model({ value: types.frozen<{ a: number; b: number } | undefined>() }) const doc = Factory.create() unprotect(doc) doc.value = { a: 1, b: 2 } expect(getSnapshot(doc)).toEqual({ value: { a: 1, b: 2 } }) }) if (process.env.NODE_ENV !== "production") { test("it should throw if value is not serializable", () => { const Factory = types.model({ value: types.frozen() }) const doc = Factory.create() unprotect(doc) expect(() => { doc.value = function IAmUnserializable() {} }).toThrow(/Error while converting to `frozen`/) }) } test("it should accept any default value value", () => { const Factory = types.model({ value: types.frozen(3) }) const doc = Factory.create() expect(Factory.is({})).toBeTruthy() expect(getSnapshot(doc)).toEqual({ value: 3 }) }) test("it should type strongly", () => { type Point = { x: number; y: number } const Mouse = types .model({ loc: types.frozen() }) .actions(self => ({ moveABit() { // self.loc.x += 1; // compile error, x is readonly! ;(self.loc as any).x += 1 // throws, frozen! } })) expect(Mouse.is({})).toBeTruthy() // any value is acceptable to frozen, even undefined... const m = Mouse.create({ // loc: 3 // type error! loc: { x: 2, y: 3 } }) if (process.env.NODE_ENV !== "production") { expect(() => { m.moveABit() }).toThrow("Attempted to assign to readonly property.") } }) if (process.env.NODE_ENV !== "production") { test("it should be capable of using another MST type", () => { const Point = types.model("Point", { x: types.number, y: types.number }) const Mouse = types.model({ loc: types.frozen(Point) }) expect(Mouse.is({})).toBeFalsy() expect(Mouse.is({ loc: {} })).toBeFalsy() expect(Mouse.is({ loc: { x: 3, y: 2 } })).toBeTruthy() expect(() => { ;(Mouse.create as any)() }).toThrow( 'at path "/loc" value `undefined` is not assignable to type: `frozen(Point)` (Value is not a plain object)' ) expect(() => { Mouse.create({ loc: { x: 4 } } as any) }).toThrow( 'at path "/loc/y" value `undefined` is not assignable to type: `number` (Value is not a number)' ) const m = Mouse.create({ loc: { x: 3, y: 2 } }) const x = m.loc.x expect(x).toBe(3) }) } ================================================ FILE: __tests__/core/hooks.test.ts ================================================ import { addDisposer, destroy, detach, types, unprotect, getSnapshot, applySnapshot, onSnapshot, isAlive, hasParent, cast, resolvePath, getParent } from "../../src" import { expect, jest, test } from "bun:test" function createTestStore(listener: (s: string) => void) { const Todo = types .model("Todo", { title: "" }) .actions(self => { function afterCreate() { listener("new todo: " + self.title) addDisposer(self, () => { listener("custom disposer 1 for " + self.title) }) addDisposer(self, () => { listener("custom disposer 2 for " + self.title) }) } function beforeDestroy() { listener("destroy todo: " + self.title) } function afterAttach() { listener("attach todo: " + self.title) } function beforeDetach() { listener("detach todo: " + self.title) } return { afterCreate, beforeDestroy, afterAttach, beforeDetach } }) const Store = types .model("Store", { todos: types.array(Todo) }) .actions(self => { function afterCreate() { unprotect(self) listener("new store: " + self.todos.length) addDisposer(self, () => { listener("custom disposer for store") }) } function beforeDestroy() { listener("destroy store: " + self.todos.length) } return { afterCreate, beforeDestroy } }) return { store: Store.create({ todos: [{ title: "Get coffee" }, { title: "Get biscuit" }, { title: "Give talk" }] }), Store, Todo } } // NOTE: as we defer creation (and thus, hooks) till first real access, // some of original hooks do not fire at all test("it should trigger lifecycle hooks", () => { const events: string[] = [] // new store: 3 const { store, Todo } = createTestStore(e => events.push(e)) events.push("-") // access (new, attach), then detach "Give Talk" const talk = detach(store.todos[2]) expect(isAlive(talk)).toBe(true) expect(hasParent(talk)).toBe(false) events.push("--") // access (new, attach), then destroy biscuit const oldBiscuit = store.todos.pop()! expect(isAlive(oldBiscuit)).toBe(false) events.push("---") // new and then attach "add sugar" const sugar = Todo.create({ title: "add sugar" }) store.todos.push(sugar) events.push("----") // destroy elements in the array ("add sugar"), then store destroy(store) expect(isAlive(store)).toBe(false) events.push("-----") // destroy "Give talk" destroy(talk) expect(isAlive(talk)).toBe(false) expect(events).toEqual([ "new store: 3", "-", "new todo: Give talk", "attach todo: Give talk", "detach todo: Give talk", "--", "new todo: Get biscuit", "attach todo: Get biscuit", "destroy todo: Get biscuit", "custom disposer 2 for Get biscuit", "custom disposer 1 for Get biscuit", "---", "new todo: add sugar", "attach todo: add sugar", "----", "destroy todo: add sugar", "custom disposer 2 for add sugar", "custom disposer 1 for add sugar", "destroy store: 2", "custom disposer for store", "-----", "destroy todo: Give talk", "custom disposer 2 for Give talk", "custom disposer 1 for Give talk" ]) }) test("lifecycle hooks can access their children", () => { const events: string[] = [] function listener(e: string) { events.push(e) } const Child = types .model("Todo", { title: "" }) .actions(self => ({ afterCreate() { listener("new child: " + self.title) }, afterAttach() { listener("parent available: " + !!getParent(self)) } })) const Parent = types .model("Parent", { child: Child }) .actions(self => ({ afterCreate() { // **This the key line**: it is trying to access the child listener("new parent, child.title: " + self.child?.title) } })) const Store = types.model("Store", { parent: types.maybe(Parent) }) const store = Store.create({ parent: { child: { title: "Junior" } } }) // As expected no hooks are called. // The `parent` is not accessed it is just loaded. events.push("-") // Simple access does a sensible thing const parent = store.parent expect(events).toEqual([ "-", "new child: Junior", "new parent, child.title: Junior", "parent available: true" ]) // Clear the events and make a new store events.length = 0 const store2 = Store.create({ parent: { child: { title: "Junior" } } }) events.push("-") // Previously resolvePath would cause problems because the parent hooks // would be called before the child was fully created const child = resolvePath(store2, "/parent/child") expect(events).toEqual([ "-", "new child: Junior", "new parent, child.title: Junior", "parent available: true" ]) }) type CarSnapshot = { id: string } const Car = types .model("Car", { id: types.number }) .preProcessSnapshot(snapshot => Object.assign({}, snapshot, { id: Number(snapshot.id) * 2 }) ) .postProcessSnapshot(snapshot => Object.assign({}, snapshot, { id: "" + snapshot.id / 2 }) ) const Factory = types.model("Factory", { car: Car }) const Motorcycle = types .model("Motorcycle", { id: types.string }) .preProcessSnapshot(snapshot => Object.assign({}, snapshot, { id: snapshot.id.toLowerCase() }) ) .postProcessSnapshot(snapshot => Object.assign({}, snapshot, { id: snapshot.id.toUpperCase() }) ) const MotorcycleFactory = types.model("MotorcycleFactory", { motorcycles: types.array(Motorcycle) }) test("it should preprocess snapshots when creating", () => { const car = Car.create({ id: "1" }) expect(car.id).toBe(2) }) test("it should preprocess snapshots when updating", () => { const car = Car.create({ id: "1" }) expect(car.id).toBe(2) applySnapshot(car, { id: "6" }) expect(car.id).toBe(12) }) test("it should postprocess snapshots when generating snapshot - 1", () => { const car = Car.create({ id: "1" }) expect(car.id).toBe(2) expect(getSnapshot(car)).toEqual({ id: "1" }) }) test("it should not apply postprocessor to snapshot on getSnapshot", () => { const car = Car.create({ id: "1" }) let error = false onSnapshot(car, snapshot => { error = true }) expect(getSnapshot(car)).toEqual({ id: "1" }) // @ts-expect-error - we are testing what happens when we explicitly do not want to apply the postprocessor, expect incorrect types expect(getSnapshot(car, false)).toEqual({ id: 2 }) expect(error).toBeFalsy() }) test("it should preprocess snapshots when creating as property type", () => { const f = Factory.create({ car: { id: "1" } }) expect(f.car.id).toBe(2) }) test("it should preprocess snapshots when updating", () => { const f = Factory.create({ car: { id: "1" } }) expect(f.car.id).toBe(2) applySnapshot(f, { car: { id: "6" } }) expect(f.car.id).toBe(12) }) test("it should postprocess snapshots when generating snapshot - 2", () => { const f = Factory.create({ car: { id: "1" } }) expect(f.car.id).toBe(2) expect(getSnapshot(f)).toEqual({ car: { id: "1" } }) }) test("it should postprocess non-initialized children", () => { const f = MotorcycleFactory.create({ motorcycles: [{ id: "a" }, { id: "b" }] }) expect(getSnapshot(f)).toEqual({ motorcycles: [{ id: "A" }, { id: "B" }] }) }) test("base hooks can be composed", () => { const events: string[] = [] function listener(message: string) { events.push(message) } const Todo = types .model("Todo", { title: "" }) .actions(self => { function afterCreate() { listener("aftercreate1") } function beforeDestroy() { listener("beforedestroy1") } function afterAttach() { listener("afterattach1") } function beforeDetach() { listener("beforedetach1") } return { afterCreate, beforeDestroy, afterAttach, beforeDetach } }) .actions(self => { function afterCreate() { listener("aftercreate2") } function beforeDestroy() { listener("beforedestroy2") } function afterAttach() { listener("afterattach2") } function beforeDetach() { listener("beforedetach2") } return { afterCreate, beforeDestroy, afterAttach, beforeDetach } }) const Store = types.model("Store", { todos: types.array(Todo) }) const store = Store.create({ todos: [] }) const todo = Todo.create() unprotect(store) store.todos.push(todo) detach(todo) destroy(todo) expect(events).toEqual([ "aftercreate1", "aftercreate2", "afterattach1", "afterattach2", "beforedetach1", "beforedetach2", "beforedestroy1", "beforedestroy2" ]) }) test("snapshot processors can be composed", () => { const X = types .model({ x: 1 }) .preProcessSnapshot(s => ({ x: s.x! - 3 })) .preProcessSnapshot(s => ({ x: s.x! / 5 })) .postProcessSnapshot(s => { return { x: s.x + 3 } }) .postProcessSnapshot(s => { return { x: s.x * 5 } }) const x = X.create({ x: 25 }) expect(x.x).toBe(2) expect(getSnapshot(x).x).toBe(25) }) test("addDisposer must return the passed disposer", () => { const listener = jest.fn() const M = types.model({}).actions(self => { expect(addDisposer(self, listener)).toBe(listener) return {} }) M.create() }) test("array calls all hooks", () => { const events: string[] = [] function listener(message: string) { events.push(message) } const Item = types.model("Item", { id: types.string }) const Collection = types.array(Item).hooks(self => ({ afterCreate() { listener("afterCreate") }, afterAttach() { listener("afterAttach") }, beforeDetach() { listener("beforeDetach") }, beforeDestroy() { listener("beforeDestroy") } })) const Holder = types.model("Holder", { items: types.maybe(Collection) }) const collection = Collection.create([{ id: "1" }, { id: "2" }, { id: "3" }]) expect(events).toStrictEqual(["afterCreate"]) const holder = Holder.create({ items: collection }) unprotect(holder) expect(events).toStrictEqual(["afterCreate", "afterAttach"]) detach(holder.items!) expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach"]) holder.items = collection expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach", "afterAttach"]) holder.items = undefined expect(events).toStrictEqual([ "afterCreate", "afterAttach", "beforeDetach", "afterAttach", "beforeDestroy" ]) }) test("map calls all hooks", () => { const events: string[] = [] function listener(message: string) { events.push(message) } const Item = types.model("Item", { id: types.string }) const Collection = types.map(Item).hooks(self => ({ afterCreate() { listener("afterCreate") }, afterAttach() { listener("afterAttach") }, beforeDetach() { listener("beforeDetach") }, beforeDestroy() { listener("beforeDestroy") } })) const Holder = types.model("Holder", { items: types.maybe(Collection) }) const collection = Collection.create({ "1": { id: "1" }, "2": { id: "2" }, "3": { id: "3" } }) expect(events).toStrictEqual(["afterCreate"]) const holder = Holder.create({ items: cast(collection) }) unprotect(holder) expect(events).toStrictEqual(["afterCreate", "afterAttach"]) detach(holder.items!) expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach"]) holder.items = collection expect(events).toStrictEqual(["afterCreate", "afterAttach", "beforeDetach", "afterAttach"]) holder.items = undefined expect(events).toStrictEqual([ "afterCreate", "afterAttach", "beforeDetach", "afterAttach", "beforeDestroy" ]) }) ================================================ FILE: __tests__/core/identifier.test.ts ================================================ import { types, tryResolve, resolvePath, SnapshotOrInstance, IAnyModelType, IAnyComplexType, resolveIdentifier, getIdentifier, detach } from "../../src" import { expect, test } from "bun:test" if (process.env.NODE_ENV !== "production") { test("#275 - Identifiers should check refinement", () => { const Model = types .model("Model", { id: types.refinement( "id", types.string, identifier => identifier.indexOf("Model_") === 0 ) }) .actions(self => ({ setId(id: string) { self.id = id } })) const ParentModel = types .model("ParentModel", { models: types.array(Model) }) .actions(self => ({ addModel(model: SnapshotOrInstance) { self.models.push(model) } })) expect(() => { ParentModel.create({ models: [{ id: "WrongId_1" }] }) }).toThrow() expect(() => { const parentStore = ParentModel.create({ models: [] }) parentStore.addModel({ id: "WrongId_2" }) }).toThrow() expect(() => { const model = Model.create({ id: "Model_1" }) model.setId("WrongId_3") }).toThrow() expect(() => { Model.create({ id: "WrongId_4" }) }).toThrow() }) } test("#158 - #88 - Identifiers should accept any string character", () => { const Todo = types.model("Todo", { id: types.identifier, title: types.string }) expect(() => { ;["coffee", "cof$fee", "cof|fee", "cof/fee"].forEach(id => { Todo.create({ id: id, title: "Get coffee" }) }) }).not.toThrow() }) test("#187 - identifiers should not break late types", () => { expect(() => { const MstNode = types.model("MstNode", { value: types.number, next: types.maybe(types.late((): IAnyModelType => MstNode)) }) }).not.toThrow() }) if (process.env.NODE_ENV !== "production") { test("should throw if multiple identifiers provided", () => { expect(() => { const Model = types.model("Model", { id: types.identifierNumber, pk: types.identifierNumber }) Model.create({ id: 1, pk: 2 }) }).toThrow( `[mobx-state-tree] Cannot define property 'pk' as object identifier, property 'id' is already defined as identifier property` ) }) test("should throw if identifier of wrong type", () => { expect(() => { const Model = types.model("Model", { id: types.identifier }) Model.create({ id: 1 as any as string }) }).toThrow( 'at path "/id" value `1` is not assignable to type: `identifier` (Value is not a valid identifier, expected a string).' ) }) test("identifier should be used only on model types - no parent provided", () => { expect(() => { const Model = types.identifierNumber Model.create(1) }).toThrow( `[mobx-state-tree] Identifier types can only be instantiated as direct child of a model type` ) }) } { const Foo = types.model("Foo", { id: types.identifier, name: types.string }) const Bar = types.model("Bar", { mimi: types.string, fooRef: types.reference(Foo) }) const Root = types.model("Root", { foos: types.map(Foo), bar: Bar }) const root = Root.create({ foos: {}, bar: { mimi: "mimi", fooRef: "123" } }) test("try resolve doesn't work #686", () => { expect(tryResolve(root, "/bar/fooRef")).toBe(undefined) expect(tryResolve(root, "/bar/fooRef/name")).toBe(undefined) }) test("try resolve shouldn't fail with a complex object node #2071", () => { const array = types.array(types.number).create([]) const model = types.model().create({}) const map = types.map(types.number).create({}) expect(tryResolve(array, "/boop")).toBeUndefined() expect(tryResolve(model, "/boop")).toBeUndefined() expect(tryResolve(map, "/boop")).toBeUndefined() }) test("failing to resolve throws sane errors", () => { expect(() => { resolvePath(root, "/bar/mimi/oopsie") }).toThrow( "[mobx-state-tree] Could not resolve 'oopsie' in path '/bar/mimi' while resolving '/bar/mimi/oopsie'" ) expect(() => { resolvePath(root, "/zoomba/moomba") }).toThrow( "[mobx-state-tree] Could not resolve 'zoomba' in path '/' while resolving '/zoomba/moomba'" ) expect(() => resolvePath(root, "/bar/fooRef")).toThrow( "[mobx-state-tree] Failed to resolve reference '123' to type 'Foo' (from node: /bar/fooRef)" ) expect(() => resolvePath(root, "/bar/fooRef/name")).toThrow( "[mobx-state-tree] Failed to resolve reference '123' to type 'Foo' (from node: /bar/fooRef)" ) }) } test("it can resolve through references", () => { const Folder = types.model("Folder", { type: types.literal("folder"), name: types.identifier, children: types.array(types.late((): IAnyComplexType => types.union(Folder, SymLink))) }) const SymLink = types.model({ type: types.literal("link"), target: types.reference(Folder) }) const root = Folder.create({ type: "folder", name: "root", children: [ { type: "folder", name: "a", children: [] }, { type: "folder", name: "b", children: [ { type: "folder", name: "c", children: [] } ] }, { type: "link", target: "b" }, { type: "link", target: "e" } ] }) expect(resolvePath(root, "/children/1/children/0").name).toBe("c") expect(resolvePath(root, "/children/2/target/children/0").name).toBe("c") expect(resolvePath(root, "/children/2/target/children/../children/./0").name).toBe("c") expect(() => resolvePath(root, "/children/3/target/children/0").name).toThrow( "[mobx-state-tree] Failed to resolve reference 'e' to type 'Folder' (from node: /children/3/target)" ) }) test("#1019", () => { let calls = 0 function randomUuid() { calls++ let pattern = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" return pattern.replace(/[xy]/g, function (cc) { const r = (Math.random() * 16) | 0, v = cc === "x" ? r : (r & 0x3) | 0x8 return v.toString(16) }) } const TOptionalId = types.optional( types.refinement(types.identifier, identifier => /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(identifier) ), randomUuid ) const CommentModel = types.model("CommentModel", { uid: TOptionalId }) const CommentStore = types .model("CommentStore", { items: types.array(CommentModel) }) .actions(self => ({ test1() { expect(calls).toBe(0) const comment = CommentModel.create({}) expect(calls).toBe(1) self.items.push(comment) const item = resolveIdentifier(CommentModel, self.items, comment.uid) const item2 = self.items.find(i => i.uid === comment.uid) expect(item).toBe(item2!) } })) const c = CommentStore.create({}) c.test1() }) test("#1019-2", () => { const Item = types.model({ id: types.optional(types.identifier, "dummykey") }) expect(getIdentifier(Item.create())).toBe("dummykey") expect(Item.create().id).toBe("dummykey") }) test("identifierAttribute of the type", () => { const M1 = types.model({}) expect(M1.identifierAttribute).toBeUndefined() const M2 = types.model({ myId: types.identifier }) expect(M2.identifierAttribute).toBe("myId") const M3 = types.model({ myId: types.optional(types.identifier, () => "hi") }) expect(M3.identifierAttribute).toBe("myId") }) test("items detached from arrays don't corrupt identifierCache", () => { const Item = types.model("Item", { id: types.identifier }) const ItemArray = types.model("ItemArray", { items: types.array(Item) }).actions(self => ({ removeSecondItemWithDetach() { detach(self.items[1]) } })) const smallArray = ItemArray.create({ items: [{ id: "A" }, { id: "B" }, { id: "C" }, { id: "D" }, { id: "E" }] }) expect(resolveIdentifier(Item, smallArray, "E")).toBeDefined() smallArray.removeSecondItemWithDetach() expect(smallArray.items.length).toBe(4) expect(resolveIdentifier(Item, smallArray, "B")).toBeUndefined() expect(resolveIdentifier(Item, smallArray, "E")).toBeDefined() const largeArray = ItemArray.create({ items: [ { id: "A" }, { id: "B" }, { id: "C" }, { id: "D" }, { id: "E" }, { id: "F" }, { id: "G" }, { id: "H" }, { id: "I" }, { id: "J" }, { id: "K" } ] }) expect(resolveIdentifier(Item, largeArray, "K")).toBeDefined() largeArray.removeSecondItemWithDetach() expect(largeArray.items.length).toBe(10) expect(resolveIdentifier(Item, largeArray, "B")).toBeUndefined() expect(resolveIdentifier(Item, largeArray, "J")).toBeDefined() // The following expectation was failing in version 5.1.8 and earlier expect(resolveIdentifier(Item, largeArray, "K")).toBeDefined() }) ================================================ FILE: __tests__/core/jsonpatch.test.ts ================================================ import { getSnapshot, unprotect, recordPatches, types, IType, IJsonPatch, Instance, cast, IAnyModelType, IMSTMap, escapeJsonPath, getPath, resolvePath, splitJsonPath, joinJsonPath } from "../../src" import { expect, test } from "bun:test" function testPatches( type: IType, snapshot: C, fn: any, expectedPatches: IJsonPatch[] ) { const instance = type.create(snapshot) const baseSnapshot = getSnapshot(instance) const recorder = recordPatches(instance) unprotect(instance) fn(instance) recorder.stop() expect(recorder.patches).toEqual(expectedPatches) const clone = type.create(snapshot) recorder.replay(clone) expect(getSnapshot(clone)).toEqual(getSnapshot(instance)) recorder.undo() expect(getSnapshot(instance)).toEqual(baseSnapshot) } const Node = types.model("Node", { id: types.identifierNumber, text: "Hi", children: types.optional(types.array(types.late((): IAnyModelType => Node)), []) }) test("it should apply simple patch", () => { testPatches( Node, { id: 1 }, (n: Instance) => { n.text = "test" }, [ { op: "replace", path: "/text", value: "test" } ] ) }) test("it should apply deep patches to arrays", () => { testPatches( Node, { id: 1, children: [{ id: 2 }] }, (n: Instance) => { const children = n.children as unknown as Instance[] children[0].text = "test" // update children[0] = cast({ id: 2, text: "world" }) // this reconciles; just an update children[0] = cast({ id: 4, text: "coffee" }) // new object children[1] = cast({ id: 3, text: "world" }) // addition children.splice(0, 1) // removal }, [ { op: "replace", path: "/children/0/text", value: "test" }, { op: "replace", path: "/children/0/text", value: "world" }, { op: "replace", path: "/children/0", value: { id: 4, text: "coffee", children: [] } }, { op: "add", path: "/children/1", value: { id: 3, text: "world", children: [] } }, { op: "remove", path: "/children/0" } ] ) }) test("it should apply deep patches to arrays with object instances", () => { testPatches( Node, { id: 1, children: [{ id: 2 }] }, (n: Instance) => { const children = n.children as unknown as Instance[] children[0].text = "test" // update children[0] = Node.create({ id: 2, text: "world" }) // this does not reconcile, new instance is provided children[0] = Node.create({ id: 4, text: "coffee" }) // new object }, [ { op: "replace", path: "/children/0/text", value: "test" }, { op: "replace", path: "/children/0", value: { id: 2, text: "world", children: [] } }, { op: "replace", path: "/children/0", value: { id: 4, text: "coffee", children: [] } } ] ) }) test("it should apply non flat patches", () => { testPatches( Node, { id: 1 }, (n: Instance) => { const children = n.children as unknown as Instance[] children.push( cast({ id: 2, children: [{ id: 4 }, { id: 5, text: "Tea" }] }) ) }, [ { op: "add", path: "/children/0", value: { id: 2, text: "Hi", children: [ { id: 4, text: "Hi", children: [] }, { id: 5, text: "Tea", children: [] } ] } } ] ) }) test("it should apply non flat patches with object instances", () => { testPatches( Node, { id: 1 }, (n: Instance) => { const children = n.children as unknown as Instance[] children.push( Node.create({ id: 2, children: [{ id: 5, text: "Tea" }] }) ) }, [ { op: "add", path: "/children/0", value: { id: 2, text: "Hi", children: [ { id: 5, text: "Tea", children: [] } ] } } ] ) }) test("it should apply deep patches to maps", () => { // If user does not transpile const/let to var, trying to call Late' subType // property getter during map's tryCollectModelTypes() will throw ReferenceError. // But if it's transpiled to var, then subType will become 'undefined'. const NodeMap = types.model("NodeMap", { id: types.identifierNumber, text: "Hi", children: types.optional(types.map(types.late((): IAnyModelType => NodeMap)), {}) }) testPatches( NodeMap, { id: 1, children: { 2: { id: 2 } } }, (n: Instance) => { const children = n.children as IMSTMap children.get("2")!.text = "test" // update children.put({ id: 2, text: "world" }) // this reconciles; just an update children.set( "4", NodeMap.create({ id: 4, text: "coffee", children: { 23: { id: 23 } } }) ) // new object children.put({ id: 3, text: "world", children: { 7: { id: 7 } } }) // addition children.delete("2") // removal }, [ { op: "replace", path: "/children/2/text", value: "test" }, { op: "replace", path: "/children/2/text", value: "world" }, { op: "add", path: "/children/4", value: { children: { 23: { children: {}, id: 23, text: "Hi" } }, id: 4, text: "coffee" } }, { op: "add", path: "/children/3", value: { children: { 7: { children: {}, id: 7, text: "Hi" } }, id: 3, text: "world" } }, { op: "remove", path: "/children/2" } ] ) }) test("it should apply deep patches to objects", () => { const NodeObject = types.model("NodeObject", { id: types.identifierNumber, text: "Hi", child: types.maybe(types.late((): IAnyModelType => NodeObject)) }) testPatches( NodeObject, { id: 1, child: { id: 2 } }, (n: Instance) => { n.child!.text = "test" // update n.child = cast({ id: 2, text: "world" }) // this reconciles; just an update n.child = NodeObject.create({ id: 2, text: "coffee", child: { id: 23 } }) n.child = cast({ id: 3, text: "world", child: { id: 7 } }) // addition n.child = undefined // removal }, [ { op: "replace", path: "/child/text", value: "test" }, { op: "replace", path: "/child/text", value: "world" }, { op: "replace", path: "/child", value: { child: { child: undefined, id: 23, text: "Hi" }, id: 2, text: "coffee" } }, { op: "replace", path: "/child", value: { child: { child: undefined, id: 7, text: "Hi" }, id: 3, text: "world" } }, { op: "replace", path: "/child", value: undefined } ] ) }) test("it should correctly split/join json patches", () => { function isValid(str: string, array: string[], altStr?: string) { expect(splitJsonPath(str)).toEqual(array) expect(joinJsonPath(array)).toBe(altStr !== undefined ? altStr : str) } isValid("", []) isValid("/", [""]) isValid("//", ["", ""]) isValid("/a", ["a"]) isValid("/a/", ["a", ""]) isValid("/a//", ["a", "", ""]) isValid(".", ["."]) isValid("..", [".."]) isValid("./a", [".", "a"]) isValid("../a", ["..", "a"]) isValid("/.a", [".a"]) isValid("/..a", ["..a"]) // rooted relatives are equivalent to plain relatives isValid("/.", ["."], ".") isValid("/..", [".."], "..") isValid("/./a", [".", "a"], "./a") isValid("/../a", ["..", "a"], "../a") function isInvalid(str: string) { expect(() => { splitJsonPath(str) }).toThrow("a json path must be either rooted, empty or relative") } isInvalid("a") isInvalid("a/") isInvalid("a//") isInvalid(".a") isInvalid(".a/") isInvalid("..a") isInvalid("..a/") }) test("it should correctly escape/unescape json patches", () => { expect(escapeJsonPath("http://example.com")).toBe("http:~1~1example.com") const AppStore = types.model({ items: types.map(types.frozen()) }) testPatches( AppStore, { items: {} }, (store: typeof AppStore.Type) => { store.items.set("with/slash~tilde", 1) }, [{ op: "add", path: "/items/with~1slash~0tilde", value: 1 }] ) }) test("weird keys are handled correctly", () => { const Store = types.model({ map: types.map( types.model({ model: types.model({ value: types.string }) }) ) }) const store = Store.create({ map: { "": { model: { value: "val1" } }, "/": { model: { value: "val2" } }, "~": { model: { value: "val3" } } } }) { const target = store.map.get("")!.model const path = getPath(target) expect(path).toBe("/map//model") expect(resolvePath(store, path)).toBe(target) } { const target = store.map.get("/")!.model const path = getPath(target) expect(path).toBe("/map/~1/model") expect(resolvePath(store, path)).toBe(target) } { const target = store.map.get("~")!.model const path = getPath(target) expect(path).toBe("/map/~0/model") expect(resolvePath(store, path)).toBe(target) } }) test("relativePath with a different base than the root works correctly", () => { const Store = types.model({ map: types.map( types.model({ model: types.model({ value: types.string }) }) ) }) const store = Store.create({ map: { "1": { model: { value: "val1" } }, "2": { model: { value: "val2" } } } }) { const target = store.map.get("1")!.model expect(resolvePath(store.map, "./1/model")).toBe(target) expect(resolvePath(store.map, "../map/1/model")).toBe(target) // rooted relative should resolve to the given base as root expect(resolvePath(store.map, "/./1/model")).toBe(target) expect(resolvePath(store.map, "/../map/1/model")).toBe(target) } { const target = store.map.get("2")!.model expect(resolvePath(store.map, "./2/model")).toBe(target) expect(resolvePath(store.map, "../map/2/model")).toBe(target) // rooted relative should resolve to the given base as root expect(resolvePath(store.map, "/./2/model")).toBe(target) expect(resolvePath(store.map, "/../map/2/model")).toBe(target) } }) test("it should emit one patch for array clear", () => { testPatches( Node, { id: 1, children: [{ id: 2 }, { id: 3 }] }, (n: Instance) => { n.children.clear() }, [ { op: "replace", path: "/children", value: [] } ] ) }) test("it should emit one patch for array replace", () => { testPatches( Node, { id: 1, children: [{ id: 2 }, { id: 3 }] }, (n: Instance) => { n.children.replace([{ id: 4 }, { id: 5 }]) }, [ { op: "replace", path: "/children", value: [ { id: 4, text: "Hi", children: [] }, { id: 5, text: "Hi", children: [] } ] } ] ) }) ================================================ FILE: __tests__/core/late.test.ts ================================================ import { types, typecheck, IAnyModelType } from "../../src" import { expect, spyOn, test } from "bun:test" if (process.env.NODE_ENV !== "production") { test("it should throw if late doesnt received a function as parameter", () => { expect(() => { types.model({ after: types.late(1 as any) }) }).toThrow() }) } test("it should accept a type and infer it correctly", () => { const Before = types.model({ after: types.late(() => After) }) const After = types.model({ name: types.maybe(types.string) }) expect(() => Before.create({ after: { name: "Hello, it's me." } })).not.toThrow() }) test("late should allow circular references", () => { // TypeScript isn't smart enough to infer self referencing types. const Node = types.model({ childs: types.optional(types.array(types.late((): IAnyModelType => Node)), []) }) expect(() => Node.create()).not.toThrow() expect(() => Node.create({ childs: [{}, { childs: [] }] })).not.toThrow() }) test("late should describe correctly circular references", () => { // TypeScript isn't smart enough to infer self referencing types. const Node = types.model("Node", { childs: types.array(types.late((): IAnyModelType => Node)) }) expect(Node.describe()).toEqual("{ childs: late(() => Node)[]? }") }) test("should typecheck", () => { const NodeObject = types.model("NodeObject", { id: types.identifierNumber, text: "Hi", child: types.maybe(types.late((): IAnyModelType => NodeObject)) }) const x = NodeObject.create({ id: 1 }) try { ;(x as any).child = 3 ;(x as any).floepie = 3 } catch (e) { // ignore, this is about TS } }) test("typecheck should throw an Error when called at runtime, but not log the error", () => { const consoleSpy = spyOn(console, "error") const NodeObject = types.model("NodeObject", { id: types.identifierNumber, text: types.string }) expect(() => { typecheck(NodeObject, { id: 1, text: 1 } as any) }).toThrow() try { typecheck(NodeObject, { id: 1, text: 1 } as any) } catch (error) { expect(error).toBeDefined() expect(consoleSpy).not.toHaveBeenCalled() } }) test("#825, late type checking ", () => { const Product = types.model({ details: types.late(() => types.optional(Details, {})) }) const Details = types.model({ name: types.maybe(types.string) }) const p2 = Product.create({}) const p = Product.create({ details: { name: "bla" } }) }) test("#916 - 0", () => { const Todo = types.model("Todo", { title: types.string, newTodo: types.optional( types.late((): IAnyModelType => Todo), {} ) // N.B. this definition is never instantiateable! }) }) test("#916 - 1", () => { const Todo = types.model("Todo", { title: types.string, newTodo: types.maybe(types.late((): IAnyModelType => Todo)) }) const t = Todo.create({ title: "Get Coffee" }) }) test("#916 - 2", () => { const Todo = types.model("Todo", { title: types.string, newTodo: types.maybe(types.late((): IAnyModelType => Todo)) }) expect( Todo.is({ title: "A", newTodo: { title: " test" } }) ).toBe(true) expect( Todo.is({ title: "A", newTodo: { title: 7 } }) ).toBe(false) }) test("#916 - 3", () => { const Todo = types.model("Todo", { title: types.string, newTodo: types.maybe(types.late((): IAnyModelType => Todo)) }) const t = Todo.create({ title: "Get Coffee", newTodo: { title: "test" } }) expect(t.newTodo!.title).toBe("test") }) ================================================ FILE: __tests__/core/lazy.test.ts ================================================ import { when } from "mobx" import { getRoot, types } from "../../src" import { expect, test } from "bun:test" interface IRootModel { shouldLoad: boolean } test("it should load the correct type", async () => { const LazyModel = types .model("LazyModel", { width: types.number, height: types.number }) .views(self => ({ get area() { return self.height * self.width } })) const Root = types .model("Root", { shouldLoad: types.optional(types.boolean, false), lazyModel: types.lazy("lazy", { loadType: () => Promise.resolve(LazyModel), shouldLoadPredicate: parent => parent.shouldLoad == true }) }) .actions(self => ({ load: () => { self.shouldLoad = true } })) const store = Root.create({ lazyModel: { width: 3, height: 2 } }) expect(store.lazyModel.width).toBe(3) expect(store.lazyModel.height).toBe(2) expect(store.lazyModel.area).toBeUndefined() store.load() await when(() => store.lazyModel && store.lazyModel.area !== undefined, { timeout: 2000 }) await expect(store.lazyModel.area).toBe(6) }) test("maintains the tree structure when loaded", async () => { const LazyModel = types .model("LazyModel", { width: types.number, height: types.number }) .views(self => ({ get area() { const root = getRoot<{ rootValue: number }>(self) return self.height * self.width * root.rootValue } })) const Root = types .model("Root", { shouldLoad: types.optional(types.boolean, false), lazyModel: types.lazy("lazy", { loadType: () => Promise.resolve(LazyModel), shouldLoadPredicate: parent => parent.shouldLoad == true }) }) .views(() => ({ get rootValue() { return 5 } })) .actions(self => ({ load: () => { self.shouldLoad = true } })) const store = Root.create({ lazyModel: { width: 3, height: 2 } }) expect(store.lazyModel.width).toBe(3) expect(store.lazyModel.height).toBe(2) expect(store.rootValue).toEqual(5) expect(store.lazyModel.area).toBeUndefined() store.load() const promise = new Promise((resolve, reject) => { when( () => store.lazyModel && store.lazyModel.area !== undefined, () => resolve(store.lazyModel.area) ) setTimeout(reject, 2000) }) await expect(promise).resolves.toBe(30) }) ================================================ FILE: __tests__/core/literal.test.ts ================================================ import { types } from "../../src" import { expect, test } from "bun:test" if (process.env.NODE_ENV !== "production") { test("it should allow only primitives", () => { const error = expect(() => { types.model({ complexArg: types.literal({ a: 1 } as any) }) }).toThrow("expected primitive as argument") }) test("it should fail if not optional and no default provided", () => { const Factory = types.literal("hello") expect(() => { ;(Factory.create as any)() }).toThrow(/is not assignable to type/) }) test("it should throw if a different type is given", () => { const Factory = types.model("TestFactory", { shouldBeOne: types.literal(1) }) expect(() => { Factory.create({ shouldBeOne: 2 as any }) }).toThrow(/is not assignable to type/) }) } test("it should support null type", () => { const M = types.model({ nullish: types.null }) expect( M.is({ nullish: null }) ).toBe(true) expect(M.is({ nullish: undefined })).toBe(false) expect(M.is({ nullish: 17 })).toBe(false) }) test("it should support undefined type", () => { const M = types.model({ undefinedish: types.undefined }) expect( M.is({ undefinedish: undefined }) ).toBe(true) expect(M.is({})).toBe(true) // MWE: disputable, should be false? expect(M.is({ undefinedish: null })).toBe(false) expect(M.is({ undefinedish: 17 })).toBe(false) }) ================================================ FILE: __tests__/core/map.test.ts ================================================ import { configure } from "mobx" import { onSnapshot, onPatch, applyPatch, applySnapshot, getSnapshot, types, unprotect, isStateTreeNode, SnapshotOut, IJsonPatch, IAnyModelType, detach } from "../../src" import { describe, expect, it, test } from "bun:test" const createTestFactories = () => { const ItemFactory = types.model({ to: "world" }) const Factory = types.map(ItemFactory) const PrimitiveMapFactory = types.model({ boolean: types.map(types.boolean), string: types.map(types.string), number: types.map(types.number) }) return { Factory, ItemFactory, PrimitiveMapFactory } } // === FACTORY TESTS === test("it should create a factory", () => { const { Factory } = createTestFactories() const snapshot = getSnapshot(Factory.create()) expect(snapshot).toEqual({}) }) test("it should succeed if not optional and no default provided", () => { const Factory = types.map(types.string) expect(Factory.create().toJSON()).toEqual({}) }) test("it should restore the state from the snapshot", () => { const { Factory } = createTestFactories() const instance = Factory.create({ hello: { to: "world" } }) expect(getSnapshot(instance)).toEqual({ hello: { to: "world" } }) expect(("" + instance).replace(/@\d+/, "@xx")).toBe("[object ObservableMap]") // default toString }) // === SNAPSHOT TESTS === test("it should emit snapshots", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) let snapshots: SnapshotOut[] = [] onSnapshot(doc, snapshot => snapshots.push(snapshot)) doc.set("hello", ItemFactory.create()) expect(snapshots).toEqual([{ hello: { to: "world" } }]) }) test("it should apply snapshots", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() applySnapshot(doc, { hello: { to: "universe" } }) expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } }) }) test("it should return a snapshot", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.set("hello", ItemFactory.create()) expect(getSnapshot(doc)).toEqual({ hello: { to: "world" } }) }) test("it should be the same each time", () => { const { PrimitiveMapFactory } = createTestFactories() const data = { string: { a: "a", b: "" }, boolean: { a: true, b: false }, number: { a: 0, b: 42, c: NaN } } const doc = PrimitiveMapFactory.create(data) expect(getSnapshot(doc)).toEqual(data) applySnapshot(doc, data) expect(getSnapshot(doc)).toEqual(data) applySnapshot(doc, data) expect(getSnapshot(doc)).toEqual(data) }) // === PATCHES TESTS === test("it should emit add patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) doc.set("hello", ItemFactory.create({ to: "universe" })) expect(patches).toEqual([{ op: "add", path: "/hello", value: { to: "universe" } }]) }) test("it should apply an add patch", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() applyPatch(doc, { op: "add", path: "/hello", value: { to: "universe" } }) expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } }) }) test("it should emit update patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.set("hello", ItemFactory.create()) let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) doc.set("hello", ItemFactory.create({ to: "universe" })) expect(patches).toEqual([{ op: "replace", path: "/hello", value: { to: "universe" } }]) }) test("it should apply an update patch", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) applyPatch(doc, { op: "replace", path: "/hello", value: { to: "universe" } }) expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } }) }) test("it should emit remove patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.set("hello", ItemFactory.create()) let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) doc.delete("hello") expect(patches).toEqual([{ op: "remove", path: "/hello" }]) }) test("it should apply a remove patch", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() unprotect(doc) doc.set("hello", ItemFactory.create()) applyPatch(doc, { op: "remove", path: "/hello" }) expect(getSnapshot(doc)).toEqual({}) }) test("it should apply patches", () => { const { Factory, ItemFactory } = createTestFactories() const doc = Factory.create() applyPatch(doc, [ { op: "add", path: "/hello", value: { to: "mars" } }, { op: "replace", path: "/hello", value: { to: "universe" } } ]) expect(getSnapshot(doc)).toEqual({ hello: { to: "universe" } }) }) // === TYPE CHECKS === test("it should check the type correctly", () => { const { Factory } = createTestFactories() const doc = Factory.create() expect(Factory.is(doc)).toEqual(true) expect(Factory.is([])).toEqual(false) expect(Factory.is({})).toEqual(true) expect(Factory.is({ hello: { to: "mars" } })).toEqual(true) expect(Factory.is({ hello: { wrongKey: true } })).toEqual(true) expect(Factory.is({ hello: { to: true } })).toEqual(false) }) test("it should support identifiers", () => { configure({ useProxies: "never" }) const Store = types.model({ todos: types.optional( types.map( types.model({ id: types.identifier }) ), {} ) }) const store = Store.create() unprotect(store) store.todos.set("17", { id: "17" }) const a = store.todos.get("17") applySnapshot(store.todos, { "16": { id: "16" }, "17": { id: "17" } }) expect(a === store.todos.get("17")).toBe(true) // same instance still expect(store.todos.get("17")!.id).toBe("17") store.todos.put({ id: "19" }) expect(store.todos.get("19")!.id).toBe("19") expect("" + store.todos.get("19")).toBe("AnonymousModel@/todos/19(id: 19)") if (process.env.NODE_ENV !== "production") { expect(() => applySnapshot(store.todos, { "17": { id: "18" } })).toThrow( "[mobx-state-tree] A map of objects containing an identifier should always store the object under their own identifier. Trying to store key '18', but expected: '17'" ) } }) test("#184 - types.map().get(key) should not throw if key doesnt exists", () => { const { Factory } = createTestFactories() const doc = Factory.create({ hello: { to: "world" } }) expect(() => { doc.get("notexistingkey") }).not.toThrow() }) test("#192 - put should not throw when identifier is a number", () => { const Todo = types.model("Todo", { todo_id: types.identifierNumber, title: types.string }) const TodoStore = types .model("TodoStore", { todos: types.optional(types.map(Todo), {}) }) .actions(self => { function addTodo(todo: typeof Todo.Type | typeof Todo.CreationType) { self.todos.put(todo) } return { addTodo } }) const todoStore = TodoStore.create({}) expect(() => { todoStore.addTodo({ todo_id: 1, title: "Test" }) }).not.toThrow() if (process.env.NODE_ENV !== "production") { expect(() => { todoStore.addTodo({ todo_id: "1", title: "Test" } as any) }).toThrow( 'at path "/todo_id" value `"1"` is not assignable to type: `identifierNumber` (Value is not a valid identifierNumber, expected a number)' ) } }) test("#192 - map should not mess up keys when putting twice", () => { const Todo = types.model("Todo", { todo_id: types.identifierNumber, title: types.string }) const TodoStore = types .model("TodoStore", { todos: types.optional(types.map(Todo), {}) }) .actions(self => { function addTodo(todo: typeof Todo.Type | typeof Todo.CreationType) { self.todos.put(todo) } return { addTodo } }) const todoStore = TodoStore.create({}) todoStore.addTodo({ todo_id: 1, title: "Test" }) expect(getSnapshot(todoStore.todos)).toEqual({ "1": { todo_id: 1, title: "Test" } }) todoStore.addTodo({ todo_id: 1, title: "Test Edited" }) expect(getSnapshot(todoStore.todos)).toEqual({ "1": { todo_id: 1, title: "Test Edited" } }) }) test("#694 - map.put should return new node", () => { configure({ useProxies: "never" }) const Todo = types.model("Todo", { todo_id: types.identifier, title: types.string }) const TodoStore = types .model("TodoStore", { todos: types.map(Todo) }) .actions(self => { function addAndReturnTodo(todo: typeof Todo.Type | typeof Todo.CreationType) { return self.todos.put(todo) } return { addAndReturnTodo } }) const todoStore = TodoStore.create({ todos: {} }) const addedTodo = todoStore.addAndReturnTodo( Todo.create({ todo_id: "1", title: "Test 1" }) ) expect(isStateTreeNode(addedTodo)).toEqual(true) expect(getSnapshot(addedTodo)).toEqual({ todo_id: "1", title: "Test 1" }) const editedTodo = todoStore.addAndReturnTodo({ todo_id: "1", title: "Test 1 Edited" }) expect(isStateTreeNode(editedTodo)).toEqual(true) expect(getSnapshot(editedTodo)).toEqual({ todo_id: "1", title: "Test 1 Edited" }) expect(editedTodo).toEqual(addedTodo) const addedTodo2 = todoStore.addAndReturnTodo({ todo_id: "2", title: "Test 2" }) expect(isStateTreeNode(addedTodo2)).toEqual(true) expect(getSnapshot(addedTodo2)).toEqual({ todo_id: "2", title: "Test 2" }) }) test("it should not throw when removing a non existing item from a map", () => { expect(() => { const AppModel = types .model({ myMap: types.map(types.number) }) .actions(self => { function something() { return self.myMap.delete("1020") } return { something } }) const store = AppModel.create() expect(store.something()).toBe(false) }).not.toThrow() }) test("it should get map keys from reversePatch when deleted an item from a nested map", () => { const AppModel = types .model({ value: types.map(types.map(types.map(types.number))) }) .actions(self => ({ remove(k: string) { self.value.delete(k) } })) const store = AppModel.create({ value: { a: { b: { c: 10 } } } }) onPatch(store, (patch, reversePatch) => { expect(patch).toEqual({ op: "remove", path: "/value/a" }) expect(reversePatch).toEqual({ op: "add", path: "/value/a", value: { b: { c: 10 } } }) }) store.remove("a") }) test("map expects regular identifiers", () => { const A = types.model("A", { a: types.identifier }) const B = types.model("B", { b: types.identifier }) // NOTE: we can determine identifier attribute upfront, so no need to wait for error while craetion expect(() => types.map(types.union(A, B))).toThrow( `[mobx-state-tree] The objects in a map should all have the same identifier attribute, expected 'a', but child of type 'B' declared attribute 'b' as identifier` ) }) test("issue #876 - map.put works fine for models with preProcessSnapshot", () => { const Note = types.model("Item", { text: types.string }) const Item = types .model("Item", { id: types.identifier, title: types.string, notes: types.array(Note) }) .preProcessSnapshot(snapshot => { const result = Object.assign({}, snapshot) if (typeof result.title !== "string") result.title = "" return result }) const Store = types .model("Store", { items: types.optional(types.map(Item), {}) }) .actions(self => ({ afterCreate() { self.items.put({ id: "1", title: "", notes: [{ text: "first note" }, { text: "second note" }] }) } })) let store!: typeof Store.Type expect(() => { store = Store.create({}) }).not.toThrow() expect(getSnapshot(store)).toEqual({ items: { "1": { id: "1", notes: [{ text: "first note" }, { text: "second note" }], title: "" } } }) }) test("map can resolve late identifiers", () => { const Late = types.model({ id: types.identifier, children: types.map(types.late((): IAnyModelType => Late)) }) const snapshot = { id: "1", children: { "2": { id: "2", children: {} } } } expect(() => Late.create(snapshot)).not.toThrow() }) test("get should return value when key is a number", () => { const Todo = types.model("Todo", { todo_id: types.identifierNumber, title: types.string }) const TodoStore = types .model("TodoStore", { todos: types.optional(types.map(Todo), {}) }) .actions(self => { function addTodo(aTodo: typeof Todo.Type | typeof Todo.CreationType) { self.todos.put(aTodo) } return { addTodo } }) const todoStore = TodoStore.create({}) const todo = { todo_id: 1, title: "Test" } todoStore.addTodo(todo) expect(todoStore.todos.get(1 as any as string)!.title).toEqual("Test") }) test("numeric keys should work", () => { const M = types.model({ id: types.identifier, title: "test" }) const S = types.model({ mies: types.map(M), ref: types.maybe(types.reference(M)) }) const s = S.create({ mies: {} }) unprotect(s) s.mies.set(7, { id: "7" }) const i7 = s.mies.get(7)! expect(i7.title).toBe("test") expect(s.mies.has("7")).toBeTruthy() expect(s.mies.has(7 as any as string)).toBeTruthy() expect(s.mies.get("7")).toBeTruthy() expect(s.mies.get(7 as any as string)).toBeTruthy() s.mies.set("8", { id: "8" }) expect(s.mies.has("8")).toBeTruthy() expect(s.mies.has(8 as any as string)).toBeTruthy() expect(s.mies.get("8")).toBeTruthy() expect(s.mies.get(8 as any as string)).toBeTruthy() expect(Array.from(s.mies.keys())).toEqual(["7", "8"]) s.mies.put({ id: "7", title: "coffee" }) expect(s.mies.size).toBe(2) expect(s.mies.has("7")).toBeTruthy() expect(s.mies.has(7 as any as string)).toBeTruthy() expect(s.mies.get("7")).toBeTruthy() expect(s.mies.get(7 as any as string)).toBeTruthy() expect(i7.title).toBe("coffee") expect(s.mies.delete(8 as any as string)).toBeTruthy() expect(s.mies.size).toBe(1) }) describe("#826, adding stuff twice", () => { const Store = types .model({ map: types.optional(types.map(types.boolean), {}) }) .actions(self => ({ toogleMap: (id: string) => { self.map.set(id, !self.map.get(id)) } })) // This one pass fine 👍 test("Toogling once shouldn't throw", () => { const store = Store.create({}) expect(() => { store.toogleMap("1") }).not.toThrow() }) // This one throws with 'Not a child 1' error 👎 test("Toogling twice shouldn't throw", () => { const store = Store.create({}) expect(() => { store.toogleMap("1") store.toogleMap("1") }).not.toThrow() }) }) test("#751 restore from snapshot should work", () => { const Submodel = types.model("Submodel", { id: types.identifierNumber }) const Model = types.model("Model", { map: types.map(Submodel) }) const server = Model.create({ map: {} }) // We add an item with a number id unprotect(server) server.map.set(1 as any as string, { id: 1 }) // We can access it using a number expect(server.map.get(1 as any as string)!.id).toBe(1) // But if we get a snapshot... const snapshot = getSnapshot(server) // And apply it back... const browser = Model.create(snapshot) // We can access it using a string expect(server.map.get("1")!.id).toBe(1) // And as number expect(server.map.get(1 as any as string)!.id).toBe(1) expect(server.map.size).toBe(1) }) test("#1173 - detaching a map should not eliminate its children", () => { const M = types.model({}) const AM = types.map(M) const Store = types.model({ items: AM }) const s = Store.create({ items: { x: {}, y: {}, z: {} } }) const n0 = s.items.get("x") unprotect(s) const detachedItems = detach(s.items) expect(s.items).not.toBe(detachedItems) expect(s.items.size).toBe(0) expect(detachedItems.size).toBe(3) expect(detachedItems.get("x")).toBe(n0!) }) test("#1131 - put with optional identifier", () => { const Test = types.model({ id: types.optional(types.identifier, () => Math.random().toString(36).substr(2)), value: "hi" }) const myMap = types.map(Test).create() unprotect(myMap) const val = myMap.put({}) expect(val.id).toBeTruthy() expect(val.value).toBe("hi") }) /** * This test exercises the TypeScript types fo `MSTMap`, to ensure that our typings stay consistent. In PR #2072, * we changed from accepting `[string, any][] | IKeyValueMap | Map | undefined,` in `initialdata` * to accepting `IObservableMapInitialValues | undefined,`. * * This test demonstrates backwards compatibility for the change, and will let us know if anything changes and breaks * if we ever update those types as well, or if MobX changes the exported `IObservableMapInitialValues` type. * * It looks like `[string, any][]` and `Map` are actually not supported, so we just test the `IKeyValueMap` and `undefined` cases * for now. See https://github.com/mobxjs/mobx-state-tree/pull/2072#issuecomment-1747482100 */ describe("#2072 - IObservableMapInitialValues types should work correctly", () => { it("should accept IKeyValueMap", () => { const initialData = { "1": "Tyler", "2": "Jamon" } const mapInstance = types.map(types.string).create(initialData) expect(mapInstance.size).toBe(2) }) it("should accept undefined", () => { const mapInstance = types.map(types.string).create(undefined) expect(mapInstance.size).toBe(0) }) }) ================================================ FILE: __tests__/core/model.test.ts ================================================ import { applySnapshot, getSnapshot, types } from "../../src" import { Hook } from "../../src/internal" import { describe, expect, it, jest, test } from "bun:test" describe("Model instantiation", () => { describe("Model name", () => { test("Providing a string as the first argument should set it as the model's name.", () => { const Model = types.model("Name", {}) expect(Model.name).toBe("Name") }) test("Providing an empty string as the first argument should set it as the model's name.", () => { const Model = types.model("", {}) expect(Model.name).toBe("") }) describe("Providing a non-string argument as the first argument should set the model's name as 'AnonymousModel'.", () => { const testCases = [ {}, null, undefined, 1, true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error(), NaN, Infinity ] testCases.forEach(testCase => { test(`Providing ${JSON.stringify( testCase )} as the first argument should set the model's name as 'AnonymousModel'.`, () => { const Model = types.model(testCase as any) expect(Model.name).toBe("AnonymousModel") }) }) }) }) describe("Model properties", () => { test("Providing a string as the first argument and an object as the second argument should use the object's properties in the model.", () => { const Model = types.model("name", { prop1: "prop1", prop2: 2 }) expect(Model.properties).toHaveProperty("prop1") expect(Model.properties).toHaveProperty("prop2") }) test("Providing an object as the first argument should parse and use its properties.", () => { const Model = types.model({ prop1: "prop1", prop2: 2 }) expect(Model.properties).toHaveProperty("prop1") expect(Model.properties).toHaveProperty("prop2") }) test("Providing a string as the first argument and a falsy value as the second argument should result in an empty set of properties.", () => { const Model = types.model("name", null as any) expect(Model.properties).toEqual({}) }) test("Model should not mutate properties object", () => { const properties = { prop1: "prop1", prop2: 2 } const Model = types.model("name", properties) expect(properties).toEqual({ prop1: "prop1", prop2: 2 }) }) }) describe("Model identifier", () => { test("If no identifier attribute is provided, the identifierAttribute should be undefined.", () => { const Model = types.model("name", {}) expect(Model.identifierAttribute).toBeUndefined() }) test("If an identifier attribute is provided, the identifierAttribute should be set for the object.", () => { const Model = types.model("name", { id: types.identifier }) expect(Model.identifierAttribute).toBe("id") }) test("If an identifier attribute has already been provided, an error should be thrown when attempting to provide a second one.", () => { expect(() => { types.model("name", { id: types.identifier, id2: types.identifier }) }).toThrow( "[mobx-state-tree] Cannot define property 'id2' as object identifier, property 'id' is already defined as identifier property" ) }) }) describe("Edge case behavior", () => { describe("when we provide no arguments to the function", () => { test("the model will be named AnonymousModel", () => { const Model = types.model() expect(Model.name).toBe("AnonymousModel") }) test("the model will have no properties", () => { const Model = types.model() const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({}) }) }) test("the model will have no properties", () => { const Model = types.model() const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({}) }) if (process.env.NODE_ENV !== "production") { test("it should not throw an error", () => { expect(() => { types.model() }).not.toThrow() }) } }) describe("when we provide an invalid name value, but a valid property object", () => { if (process.env.NODE_ENV === "production") { test("the model will be named AnonymousModel", () => { const Model = types.model(null as any, { prop1: "prop1", prop2: 2 }) expect(Model.name).toBe("AnonymousModel") }) test("the model will have no properties", () => { const Model = types.model(null as any, { prop1: "prop1", prop2: 2 }) const modelSnapshot = getSnapshot(Model.create()) // @ts-expect-error - we explicitly allowed an invalid input, so we expect an empty object, but TS doesn't. expect(modelSnapshot).toEqual({}) }) } else { test("it should complain about invalid name", () => { expect(() => { types.model(null as any, { prop1: "prop1", prop2: 2 }) }).toThrow( "[mobx-state-tree] Model creation failed. First argument must be a string when two arguments are provided" ) }) } }) describe("when we provide three arguments to the function", () => { test("the model gets the correct name", () => { // @ts-ignore const Model = types.model("name", {}, {}) expect(Model.name).toBe("name") }) test("the model gets the correct properties", () => { const Model = types.model( "name", { prop1: "prop1", prop2: 2 }, // @ts-ignore {} ) const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({ prop1: "prop1", prop2: 2 }) }) }) test("it should call preProcessSnapshot with the correct argument", () => { const onSnapshot = jest.fn((snapshot: any) => { return { val: snapshot.val + 1 } }) const Model = types .model({ val: types.number }) .preProcessSnapshot(onSnapshot) const model = Model.create({ val: 0 }) applySnapshot(model, { val: 1 }) expect(onSnapshot).toHaveBeenLastCalledWith({ val: 1 }) }) describe("Should show a friendly message when a model has a property overridden by", () => { test("a view", () => { const UserModel = types .model("UserModel", { id: types.identifier, name: types.string }) .views(user => ({ get name() { return user.name } })) expect(() => UserModel.create({ id: "chakri", name: "Subramanya Chakravarthy" }) ).toThrow("[mobx-state-tree] 'name' is a property and cannot be declared as a view") }) test("an action", () => { const StringSet = types .model("StringSet", { setName: types.string, items: types.array(types.string) }) .actions(self => ({ setName(name: string) { self.setName = name } })) expect(() => StringSet.create({ setName: "Fruits", items: ["banana", "apple"] }) ).toThrow( "[mobx-state-tree] 'setName' is a property and cannot be declared as an action" ) }) test("a volatile", () => { const UserModel = types .model("UserModel", { id: types.identifier, name: types.string }) .volatile(_self => ({ name: "Subramanya Chakravarthy" })) expect(() => UserModel.create({ id: "chakri", name: "Subramanya Chakravarthy" }) ).toThrow( "[mobx-state-tree] 'name' is a property and cannot be declared as volatile state" ) }) }) describe("with all of the property types", () => { const IdentifiedWithString = types.model({ id: types.identifier }) const IdentifiedWithNumber = types.model({ id: types.identifierNumber }) const Custom = types.custom({ name: "angle bracketed", fromSnapshot(snapshot, _env) { return `<${snapshot}>` }, toSnapshot(value) { return value.slice(1, -1) }, isTargetType(value): boolean { return value.startsWith("<") && value.endsWith(">") }, getValidationMessage(snapshot): string { if (typeof snapshot == "string") return "" throw new Error(`${snapshot} missing surrounding angle brackets`) } }) const Everything = types.model({ boolean: types.boolean, custom: Custom, Date: types.Date, enumeration: types.enumeration(["A", "B"]), float: types.float, finite: types.finite, frozen: types.frozen<{ s: string }>(), integer: types.integer, late: types.late(() => types.string), lazy: types.lazy("lazy", { loadType: () => Promise.resolve(types.string), shouldLoadPredicate: () => true }), literal: types.literal("literal"), maybe: types.maybe(types.string), maybeNull: types.maybeNull(types.number), null: types.null, number: types.number, optional: types.optional(types.string, "default"), reference: types.reference(IdentifiedWithString), refinement: types.refinement(types.string, s => s.length > 2), string: types.string, safeReference: types.safeReference(IdentifiedWithNumber), undefined: types.undefined, union: types.union(types.string, types.number) } satisfies Record< Exclude< keyof typeof types, | "compose" | "model" | "identifier" | "identifierNumber" | "bigint" | "map" | "array" | "snapshotProcessor" >, any >) const Root = types.model({ everything: types.snapshotProcessor(Everything, { preProcessor(snapshot) { if (snapshot.refinement.length < 2) { return { ...snapshot, refinement: "" } } return snapshot } }), mapOfStrings: types.map(IdentifiedWithString), arrayOfNumbers: types.array(IdentifiedWithNumber) }) it("does not throw with input snapshots", () => { const value = Root.create({ everything: { boolean: true, custom: "custom", Date: 0, enumeration: "A", float: 1.23, finite: 1, frozen: { s: "test" }, integer: 1, late: "test", lazy: "test", literal: "literal", maybe: "test", maybeNull: 1, null: null, number: 1, optional: "test", reference: "id-a", refinement: "test", string: "test", safeReference: 1, undefined: undefined, union: "test" }, mapOfStrings: { "id-a": { id: "id-a" } }, arrayOfNumbers: [{ id: 1 }] }) expect(getSnapshot(value)).toEqual({ everything: { boolean: true, custom: "custom", Date: 0, enumeration: "A", float: 1.23, finite: 1, frozen: { s: "test" }, integer: 1, late: "test", lazy: "test", literal: "literal", maybe: "test", maybeNull: 1, null: null, number: 1, optional: "test", reference: "id-a", refinement: "test", string: "test", safeReference: 1, undefined: undefined, union: "test" }, mapOfStrings: { "id-a": { id: "id-a" } }, arrayOfNumbers: [{ id: 1 }] }) }) it("does not throw with input instances", () => { const instanceA = IdentifiedWithString.create({ id: "id-a" }) const instance1 = IdentifiedWithNumber.create({ id: 1 }) const value = Root.create({ everything: { boolean: types.boolean.create(true), custom: Custom.create("custom"), Date: types.Date.create(0), enumeration: types.enumeration(["A", "B"]).create("A"), float: types.float.create(1.23), finite: types.finite.create(1), frozen: types.frozen<{ s: string }>().create({ s: "test" }), integer: types.integer.create(1), late: types.string.create("test"), lazy: types.string.create("test"), literal: types.literal("literal").create("literal"), maybe: types.maybe(types.string).create("test"), maybeNull: types.maybeNull(types.number).create(1), null: types.null.create(null), number: types.number.create(1), optional: types.optional(types.string, "default").create("test"), reference: instanceA, refinement: types.refinement(types.string, s => s.length > 2).create("test"), string: types.string.create("test"), safeReference: instance1, undefined: types.undefined.create(undefined), union: types.union(types.string, types.number).create("test") }, mapOfStrings: { "id-a": instanceA }, arrayOfNumbers: [instance1] }) expect(getSnapshot(value)).toEqual({ everything: { boolean: true, custom: "custom", Date: 0, enumeration: "A", float: 1.23, finite: 1, frozen: { s: "test" }, integer: 1, late: "test", lazy: "test", literal: "literal", maybe: "test", maybeNull: 1, null: null, number: 1, optional: "test", reference: "id-a", refinement: "test", string: "test", safeReference: 1, undefined: undefined, union: "test" }, mapOfStrings: { "id-a": { id: "id-a" } }, arrayOfNumbers: [{ id: 1 }] }) }) }) }) describe("Model properties objects", () => { describe("when a user names a property the same as an MST lifecycle hook", () => { test("it throws an error", () => { const hookValues = Object.values(Hook) hookValues.forEach(hook => { expect(() => { types.model({ [hook]: types.string }) }).toThrow() }) }) }) describe("when a user attempts to define a property with the get keyword", () => { test("it throws an error", () => { expect(() => { types.model({ get foo() { return "bar" } }) }).toThrow( "[mobx-state-tree] Getters are not supported as properties. Please use views instead" ) }) }) describe("when a user attempts to define a property with null as the value", () => { test("it throws an error", () => { expect(() => { types.model({ foo: null as any }) }).toThrow( "[mobx-state-tree] The default value of an attribute cannot be null or undefined as the type cannot be inferred. Did you mean `types.maybe(someType)`?" ) }) }) describe("when a user attempts to define a property with undefined as the value", () => { test("it throws an error", () => { expect(() => { types.model({ foo: undefined as any }) }).toThrow( "[mobx-state-tree] The default value of an attribute cannot be null or undefined as the type cannot be inferred. Did you mean `types.maybe(someType)`?" ) }) }) describe("when a user defines a property using a primitive value (not null or undefined)", () => { describe("and the primitive value is a string", () => { test("it converts a string to an optional string", () => { const Model = types.model({ foo: "bar" }) const modelDescription = Model.describe() expect(modelDescription).toBe("{ foo: string? }") }) test("it uses the primitive value as the default value", () => { const Model = types.model({ foo: "bar" }) const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({ foo: "bar" }) }) }) describe("and the primitive value is a number", () => { test("it converts a number to an optional number", () => { const Model = types.model({ foo: 1 }) const modelDescription = Model.describe() expect(modelDescription).toBe("{ foo: number? }") }) test("it uses the primitive value as the default value", () => { const Model = types.model({ foo: 1 }) const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({ foo: 1 }) }) }) describe("and the primitive value is a boolean", () => { test("it converts a boolean to an optional boolean", () => { const Model = types.model({ foo: true }) const modelDescription = Model.describe() expect(modelDescription).toBe("{ foo: boolean? }") }) test("it uses the primitive value as the default value", () => { const Model = types.model({ foo: true }) const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({ foo: true }) }) }) describe("and the primitive value is a date", () => { test("it converts a date to an optional date", () => { const Model = types.model({ foo: new Date() }) const modelDescription = Model.describe() expect(modelDescription).toBe("{ foo: Date? }") }) test("it sets a default value with the date in unix milliseconds timestamp", () => { const date = new Date("2023-07-24T04:26:04.701Z") const Model = types.model({ foo: date }) const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({ foo: 1690172764701 }) }) }) }) describe("when a user defines a property using a complex type", () => { describe('and that type is "types.map"', () => { test("it sets the default value to an empty map", () => { const Model = types.model({ foo: types.map(types.string) }) const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({ foo: {} }) }) }) describe('and that type is "types.array"', () => { test("it sets the default value to an empty array", () => { const Model = types.model({ foo: types.array(types.string) }) const modelSnapshot = getSnapshot(Model.create()) expect(modelSnapshot).toEqual({ foo: [] }) }) }) describe("and that type is another model", () => { test("it sets the default value to the default of that model", () => { const Todo = types.model({ task: types.optional(types.string, "test") }) const TodoStore = types.model("TodoStore", { todo1: types.optional(Todo, () => Todo.create()) }) const modelSnapshot = getSnapshot(TodoStore.create()) expect(modelSnapshot).toEqual({ todo1: { task: "test" } }) }) }) }) describe("when a user defines a property using a function", () => { if (process.env.NODE_ENV !== "production") { test("it throws an error when not in production", () => { expect(() => { // @ts-ignore types.model({ foo: () => "bar" }) }).toThrow( "[mobx-state-tree] Invalid type definition for property 'foo', it looks like you passed a function. Did you forget to invoke it, or did you intend to declare a view / action?" ) }) } }) describe("when a user defines a property using a plain JavaScript object", () => { if (process.env.NODE_ENV !== "production") { test("it throws an error when not in production", () => { expect(() => { // @ts-ignore types.model({ foo: {} }) }).toThrow() }) } }) describe("when a user uses `.props` to create a child model", () => { it("does not modify the parent properties", () => { const Parent = types.model({ first: types.string }) const Child = Parent.props({ second: types.string }) expect(Parent.properties).not.toHaveProperty("second") }) }) }) ================================================ FILE: __tests__/core/name.test.ts ================================================ import { types } from "../../src" import { getDebugName } from "mobx" import { expect, test } from "bun:test" test("it should have a debug name", () => { const Model = types.model("Name") const model = Model.create() const array = types.array(Model).create() const map = types.map(Model).create() expect(getDebugName(model)).toBe("Name") expect(getDebugName(array)).toBe("Name[]") expect(getDebugName(map)).toBe("Map") }) ================================================ FILE: __tests__/core/node.test.ts ================================================ import { getPath, getSnapshot, getParent, hasParent, getRoot, getIdentifier, getPathParts, isAlive, clone, getType, getChildType, recordActions, recordPatches, types, destroy, unprotect, hasParentOfType, getParentOfType, detach, getNodeId } from "../../src" import { autorun, configure } from "mobx" import { expect, test } from "bun:test" // getParent test("it should resolve to the parent instance", () => { const Row = types.model({ article_id: 0 }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) expect(getParent(row)).toEqual(doc.rows) }) // hasParent test("it should check for parent instance", () => { const Row = types.model({ article_id: 0 }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) expect(hasParent(row)).toEqual(true) }) test("it should check for parent instance (unbound)", () => { const Row = types.model({ article_id: 0 }) const row = Row.create() expect(hasParent(row)).toEqual(false) }) // getParentOfType test("it should resolve to the given parent instance", () => { configure({ useProxies: "never" }) const Cell = types.model({}) const Row = types.model({ cells: types.optional(types.array(Cell), []) }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create({ rows: [ { cells: [{}] } ] }) expect(getParentOfType(doc.rows[0].cells[0], Document)).toEqual(doc) }) test("it should throw if there is not parent of type", () => { const Cell = types.model({}) const Row = types.model({ cells: types.optional(types.array(Cell), []) }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const row = Row.create({ cells: [{}] }) expect(() => getParentOfType(row.cells[0], Document)).toThrow( "[mobx-state-tree] Failed to find the parent of AnonymousModel@/cells/0 of a given type" ) }) // hasParentOfType test("it should check for parent instance of given type", () => { const Cell = types.model({}) const Row = types.model({ cells: types.optional(types.array(Cell), []) }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create({ rows: [ { cells: [{}] } ] }) expect(hasParentOfType(doc.rows[0].cells[0], Document)).toEqual(true) }) test("it should check for parent instance of given type (unbound)", () => { const Cell = types.model({}) const Row = types.model({ cells: types.optional(types.array(Cell), []) }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const row = Row.create({ cells: [{}] }) expect(hasParentOfType(row.cells[0], Document)).toEqual(false) }) // getRoot test("it should resolve to the root of an object", () => { const Row = types.model("Row", { article_id: 0 }) const Document = types.model("Document", { rows: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) expect(getRoot(row)).toBe(doc) }) // getIdentifier test("it should resolve to the identifier of the object", () => { const Document = types.model("Document", { id: types.identifier }) const doc = Document.create({ id: "document_1" }) // get identifier of object expect(getIdentifier(doc)).toBe("document_1") }) // getPath test("it should resolve the path of an object", () => { const Row = types.model({ article_id: 0 }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) expect(getPath(row)).toEqual("/rows/0") }) // getPathParts test("it should resolve the path of an object", () => { const Row = types.model({ article_id: 0 }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) expect(getPathParts(row)).toEqual(["rows", "0"]) }) test("it should resolve parents", () => { const Row = types.model({ article_id: 0 }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) expect(hasParent(row)).toBe(true) // array expect(hasParent(row, 2)).toBe(true) // row expect(hasParent(row, 3)).toBe(false) expect(getParent(row) === doc.rows).toBe(true) // array expect(getParent(row, 2) === doc).toBe(true) // row expect(() => getParent(row, 3)).toThrow( "[mobx-state-tree] Failed to find the parent of AnonymousModel@/rows/0 at depth 3" ) }) // clone test("it should clone a node", () => { configure({ useProxies: "never" }) const Row = types.model({ article_id: 0 }) const Document = types.model({ rows: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) const cloned = clone(doc) expect(doc).toEqual(cloned) expect(getSnapshot(doc)).toEqual(getSnapshot(cloned)) }) test("it should be possible to clone a dead object", () => { configure({ useProxies: "never" }) const Task = types.model("Task", { x: types.string }) const a = Task.create({ x: "a" }) const store = types .model({ todos: types.optional(types.array(Task), []) }) .create({ todos: [a] }) unprotect(store) expect(store.todos.slice()).toEqual([a]) expect(isAlive(a)).toBe(true) store.todos.splice(0, 1) expect(isAlive(a)).toBe(false) const a2 = clone(a) store.todos.splice(0, 0, a2) expect(store.todos[0].x).toBe("a") }) // getModelFactory test("it should return the model factory", () => { const Document = types.model({ customer_id: 0 }) const doc = Document.create() expect(getType(doc)).toEqual(Document) }) // getChildModelFactory test("it should return the child model factory", () => { const Row = types.model({ article_id: 0 }) const ArrayOfRow = types.optional(types.array(Row), []) const Document = types.model({ rows: ArrayOfRow }) const doc = Document.create() expect(getChildType(doc, "rows")).toEqual(ArrayOfRow) }) test("a node can exists only once in a tree", () => { const Row = types.model({ article_id: 0 }) const Document = types.model({ rows: types.optional(types.array(Row), []), foos: types.optional(types.array(Row), []) }) const doc = Document.create() unprotect(doc) const row = Row.create() doc.rows.push(row) expect(() => { doc.foos.push(row) }).toThrow( "[mobx-state-tree] Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '/foos/0', but it lives already at '/rows/0'" ) }) test("make sure array filter works properly", () => { const Row = types.model({ done: false }) const Document = types .model({ rows: types.optional(types.array(Row), []) }) .actions(self => { function clearDone() { self.rows.filter(row => row.done === true).forEach(destroy) } return { clearDone } }) const doc = Document.create() unprotect(doc) const a = Row.create({ done: true }) const b = Row.create({ done: false }) doc.rows.push(a) doc.rows.push(b) doc.clearDone() expect(getSnapshot(doc)).toEqual({ rows: [{ done: false }] }) }) // === RECORD PATCHES === test("it can record and replay patches", () => { const Row = types.model({ article_id: 0 }) const Document = types.model({ customer_id: 0, rows: types.optional(types.array(Row), []) }) const source = Document.create() unprotect(source) const target = Document.create() const recorder = recordPatches(source) source.customer_id = 1 source.rows.push(Row.create({ article_id: 1 })) recorder.replay(target) expect(getSnapshot(source)).toEqual(getSnapshot(target)) }) // === RECORD ACTIONS === test("it can record and replay actions", () => { const Row = types .model({ article_id: 0 }) .actions(self => { function setArticle(article_id: number) { self.article_id = article_id } return { setArticle } }) const Document = types .model({ customer_id: 0, rows: types.optional(types.array(Row), []) }) .actions(self => { function setCustomer(customer_id: number) { self.customer_id = customer_id } function addRow() { self.rows.push(Row.create()) } return { setCustomer, addRow } }) const source = Document.create() const target = Document.create() const recorder = recordActions(source) source.setCustomer(1) source.addRow() source.rows[0].setArticle(1) recorder.replay(target) expect(getSnapshot(source)).toEqual(getSnapshot(target)) }) test("Liveliness issue #683", () => { const User = types.model({ id: types.identifierNumber, name: types.string }) const Users = types .model({ list: types.map(User) }) .actions(self => ({ put(aUser: typeof User.CreationType | typeof User.Type) { // if (self.has(user.id)) detach(self.get(user.id)); self.list.put(aUser) }, get(id: string) { return self.list.get(id) }, has(id: string) { return self.list.has(id) } })) const users = Users.create({ list: { 1: { name: "Name", id: 1 } } }) const user = users.get("1") expect(user!.name).toBe("Name") users.put({ id: 1, name: "NameX" }) expect(user!.name).toBe("NameX") expect(users.get("1")!.name).toBe("NameX") }) test("triggers on changing paths - 1", () => { const Todo = types.model({ title: types.string }) const App = types .model({ todos: types.array(Todo) }) .actions(self => ({ do(fn: () => void) { fn() } })) const t1 = Todo.create({ title: "t1 " }) const t2 = Todo.create({ title: "t2 " }) const app = App.create({ todos: [t1] }) const events: string[] = [] const d1 = autorun(() => { events.push("t1@" + getPath(t1)) }) const d2 = autorun(() => { events.push("t2@" + getPath(t2)) }) expect(events.splice(0)).toEqual(["t1@/todos/0", "t2@"]) app.do(() => { app.todos.unshift(t2) }) expect(events.splice(0)).toEqual(["t2@/todos/0", "t1@/todos/1"]) app.do(() => { detach(t2) }) expect(events.splice(0)).toEqual(["t1@/todos/0", "t2@"]) app.do(() => { app.todos.splice(0) }) expect(events.splice(0)).toEqual(["t1@"]) }) test("getNodeId works", () => { const M = types.model({}) const m1 = M.create() const m2 = M.create() const m1Id = getNodeId(m1) const m2Id = getNodeId(m2) expect(m1Id).toBeGreaterThan(0) expect(m2Id).toBe(m1Id + 1) }) ================================================ FILE: __tests__/core/number.test.ts ================================================ import { t } from "../../src" import { Hook, NodeLifeCycle } from "../../src/internal" import { describe, it, expect, test } from "bun:test" describe("types.number", () => { describe("methods", () => { describe("create", () => { describe("with no arguments", () => { if (process.env.NODE_ENV !== "production") { it("should throw an error in development", () => { expect(() => { t.number.create() }).toThrow() }) } }) describe("with a number argument", () => { it("should return a number", () => { const n = t.number.create(1) expect(typeof n).toBe("number") }) }) describe("with argument of different types", () => { // Keep in mind, Infinity and NaN are treated as numbers in JavaScript, so we won't test for them here. const testCases = [ null, undefined, "string", true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error() ] if (process.env.NODE_ENV !== "production") { testCases.forEach(testCase => { it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => { expect(() => { t.number.create(testCase as any) }).toThrow() }) }) } }) }) describe("describe", () => { it("should return the value 'number'", () => { const description = t.number.describe() expect(description).toBe("number") }) }) describe("getSnapshot", () => { it("should return the value passed in", () => { const n = t.number.instantiate(null, "", {}, 1) const snapshot = t.number.getSnapshot(n) expect(snapshot).toBe(1) }) }) describe("getSubtype", () => { it("should return null", () => { const subtype = t.number.getSubTypes() expect(subtype).toBe(null) }) }) describe("instantiate", () => { if (process.env.NODE_ENV !== "production") { describe("with invalid arguments", () => { it("should not throw an error", () => { expect(() => { // @ts-ignore t.number.instantiate() }).not.toThrow() }) }) } describe("with a number argument", () => { it("should return an object", () => { const n = t.number.instantiate(null, "", {}, 1) expect(typeof n).toBe("object") }) }) }) describe("is", () => { describe("with a number argument", () => { it("should return true", () => { const result = t.number.is(1) expect(result).toBe(true) }) }) describe("with argument of different types", () => { // Keep in mind, Infinity and NaN are treated as numbers in JavaScript, so we won't test for them here. const testCases = [ null, undefined, "string", true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error() ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.number.is(testCase as any) expect(result).toBe(false) }) }) }) }) describe("isAssignableFrom", () => { describe("with a number argument", () => { it("should return true", () => { const result = t.number.isAssignableFrom(t.number) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ t.Date, t.boolean, t.finite, t.float, t.identifier, t.identifierNumber, t.integer, t.null, t.string, t.undefined ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = t.number.isAssignableFrom(testCase as any) expect(result).toBe(false) }) }) }) }) // TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single number nodes. describe.skip("reconcile", () => {}) describe("validate", () => { describe("with a number argument", () => { it("should return with no validation errors", () => { const result = t.number.validate(1, []) expect(result).toEqual([]) }) }) describe("with argument of different types", () => { // Keep in mind, Infinity and NaN are treated as numbers in JavaScript, so we won't test for them here. const testCases = [ null, undefined, "string", true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error() ] testCases.forEach(testCase => { it(`should return with a validation error when passed ${JSON.stringify( testCase )}`, () => { const result = t.number.validate(testCase as any, []) expect(result).toEqual([ { context: [], message: "Value is not a number", value: testCase } ]) }) }) }) }) }) describe("properties", () => { describe("flags", () => { test("return the correct value", () => { const flags = t.number.flags expect(flags).toBe(2) }) }) describe("identifierAttribute", () => { // We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined. test("returns undefined", () => { const identifierAttribute = t.number.identifierAttribute expect(identifierAttribute).toBeUndefined() }) }) describe("isType", () => { test("returns true", () => { const isType = t.number.isType expect(isType).toBe(true) }) }) describe("name", () => { test('returns "number"', () => { const name = t.number.name expect(name).toBe("number") }) }) }) describe("instance", () => { describe("methods", () => { describe("aboutToDie", () => { it("calls the beforeDetach hook", () => { const n = t.number.instantiate(null, "", {}, 1) let called = false n.registerHook(Hook.beforeDestroy, () => { called = true }) n.aboutToDie() expect(called).toBe(true) }) }) describe("die", () => { it("kills the node", () => { const n = t.number.instantiate(null, "", {}, 1) n.die() expect(n.isAlive).toBe(false) }) it("should mark the node as dead", () => { const n = t.number.instantiate(null, "", {}, 1) n.die() expect(n.state).toBe(NodeLifeCycle.DEAD) }) }) describe("finalizeCreation", () => { it("should mark the node as finalized", () => { const n = t.number.instantiate(null, "", {}, 1) n.finalizeCreation() expect(n.state).toBe(NodeLifeCycle.FINALIZED) }) }) describe("finalizeDeath", () => { it("should mark the node as dead", () => { const n = t.number.instantiate(null, "", {}, 1) n.finalizeDeath() expect(n.state).toBe(NodeLifeCycle.DEAD) }) }) describe("getReconciliationType", () => { it("should return the correct type", () => { const n = t.number.instantiate(null, "", {}, 1) const type = n.getReconciliationType() expect(type).toBe(t.number) }) }) describe("getSnapshot", () => { it("should return the value passed in", () => { const n = t.number.instantiate(null, "", {}, 1) const snapshot = n.getSnapshot() expect(snapshot).toBe(1) }) }) describe("registerHook", () => { it("should register a hook and call it", () => { const n = t.number.instantiate(null, "", {}, 1) let called = false n.registerHook(Hook.beforeDestroy, () => { called = true }) n.die() expect(called).toBe(true) }) }) describe("setParent", () => { if (process.env.NODE_ENV !== "production") { describe("with null", () => { it("should throw an error", () => { const n = t.number.instantiate(null, "", {}, 1) expect(() => { n.setParent(null, "foo") }).toThrow() }) }) describe("with a parent object", () => { it("should throw an error", () => { const Parent = t.model({ child: t.number }) const parent = Parent.create({ child: 1 }) const n = t.number.instantiate(null, "", {}, 1) expect(() => { // @ts-ignore n.setParent(parent, "bar") }).toThrow( "[mobx-state-tree] assertion failed: scalar nodes cannot change their parent" ) }) }) } }) }) }) }) ================================================ FILE: __tests__/core/object-node.test.ts ================================================ import { t } from "../../src/index" import { Hook, ObjectNode, onPatch, unprotect } from "../../src/internal" import { describe, expect, jest, it, spyOn } from "bun:test" const TestModel = t.model("TestModel", { title: t.string }) const TestArray = t.array(TestModel) const TestMap = t.map(TestModel) const Parent = t.model("Parent", { child: t.maybe(TestModel) }) const TestModelWithIdentifier = t.model("TestModelWithIdentifier", { id: t.identifier, title: t.string }) /** * These tests were added to help understand the ObjectNode class and how it interacts internally in MST. * As such, they test internals of the library that aren't intended to be used by consumers. That means: * * 1. Please do not use these examples in application-level code with MST, these are not necessarily best practices, and are definitely not intended as a public API * 2. These tests may not test every single case, but they cover scenarios I was interested in understanding. * 3. Since the tests are tightly coupled to implementation, this test suite may end up being noisy. * * If you are making a change to MST and get failures here, please consider that as a yellow flag, not a red flag. * Feel free to make changes you need to, or even skip tests if they're a nuisance. */ describe("ObjectNode", () => { describe("constructor", () => { // Since ObjectNode is not exported as part of the MST API, we don't have tests for invalid parameters, but we expect an error in this scenario. it("throws if type is not a complex type", () => { expect(() => new ObjectNode(t.string as any, null, "", {}, "foo")).toThrow( "complexType.initializeChildNodes is not a function" ) }) it("works with a complex type", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node).toBeDefined() }) }) describe("methods", () => { describe("aboutToDie", () => { describe("if the observable node is unitialized", () => { it("does not call the onAboutToDie hook", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.beforeDestroy, hook) node.aboutToDie() expect(hook).not.toHaveBeenCalled() }) }) describe("if the observable node is initialized", () => { it("calls the onAboutToDie hook", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.beforeDestroy, hook) node.createObservableInstance() // createObservableInstance calls finalizeCreation internally, and marks the observable node as being created. node.aboutToDie() expect(hook).toHaveBeenCalled() }) }) }) describe("addDisposer", () => { it("adds a disposer to the node", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const disposer = jest.fn() node.addDisposer(disposer) node.createObservableInstance() expect(node.hasDisposer(disposer)).toBe(true) }) }) describe("addMiddleWare", () => { it("adds a middleware to the node", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const middleware = jest.fn((call, next) => { next(call) }) node.addMiddleWare(middleware) node.createObservableInstance() node.applySnapshot({ title: "hello" } as any) expect(middleware).toHaveBeenCalled() }) }) describe("applyPatchLocally", () => { describe("when the node is protected", () => { it("throws an error", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(() => node.applyPatchLocally("", { op: "replace", path: "", value: { title: "hello" } }) ).toThrow( "[mobx-state-tree] Cannot modify 'TestModel@', the object is protected and can only be modified by using an action." ) }) }) describe("when the node is not alive", () => { it("warns by default", () => { const warnSpy = spyOn(console, "warn") const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.die() node.applyPatchLocally("", { op: "replace", path: "", value: { title: "hello" } }) expect(warnSpy).toHaveBeenCalled() }) }) describe("when the node is alive and not protected", () => { describe("for models", () => { it("does not allow remove", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected expect(() => { node.applyPatchLocally("", { op: "remove", path: "" }) }).toThrow("[mobx-state-tree] object does not support operation remove") }) it("allows add", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("title", { op: "add", path: "", value: "world" }) // @ts-ignore expect(node.storedValue.title).toBe("world") }) it("allows replace", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("title", { op: "replace", path: "", value: "world" }) // @ts-ignore expect(node.storedValue.title).toBe("world") }) }) describe("for arrays", () => { it("works for replace", () => { const node = new ObjectNode(TestArray as any, null, "", {}, [ { title: "hello" } ]) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("0", { op: "replace", path: "", value: { title: "world" } }) // @ts-ignore expect(node.storedValue[0].title).toBe("world") }) it("works for add", () => { const node = new ObjectNode(TestArray as any, null, "", {}, [ { title: "hello" } ]) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("1", { op: "add", path: "", value: { title: "world" } }) // @ts-ignore expect(node.storedValue.length).toBe(2) // @ts-ignore expect(node.storedValue[1].title).toBe("world") }) it("works for remove", () => { const node = new ObjectNode(TestArray as any, null, "", {}, [ { title: "hello" } ]) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("0", { op: "remove", path: "" }) // @ts-ignore expect(node.storedValue.length).toBe(0) }) }) describe("for maps", () => { it("works for add", () => { const node = new ObjectNode( TestMap as any, null, "", {}, { hello: { title: "hello" } } ) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("world", { op: "add", path: "", value: { title: "world" } }) // @ts-ignore expect(node.storedValue.get("world").title).toBe("world") }) it("works for replace", () => { const node = new ObjectNode( TestMap as any, null, "", {}, { hello: { title: "hello" } } ) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("hello", { op: "replace", path: "", value: { title: "world" } }) // @ts-ignore expect(node.storedValue.get("hello").title).toBe("world") }) it("works for remove", () => { const node = new ObjectNode( TestMap as any, null, "", {}, { hello: { title: "hello" } } ) unprotect(node.root.value) // In order to call applyPatchLocally, the node must be unprotected node.applyPatchLocally("hello", { op: "remove", path: "" }) // @ts-ignore expect(node.storedValue.size).toBe(0) }) }) }) }) describe("applyPatches", () => { describe("when the path is not specified", () => { it("applies the value as a snapshot", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) // Notice we're saying to "remove" the value, but using just a snapshot. This will just apply a snapshot based on how applyPatches runs. // @ts-ignore node.applyPatches([{ op: "remove", value: { title: "world" } }]) // @ts-ignore expect(node.storedValue.title).toBe("world") }) }) describe("with correct paths", () => { it("applies the patch", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.applyPatches([{ op: "replace", path: "/title", value: "world" }]) // @ts-ignore expect(node.storedValue.title).toBe("world") }) }) }) describe("applySnapshot", () => { it("works", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.applySnapshot({ title: "world" }) // @ts-ignore expect(node.storedValue.title).toBe("world") }) }) describe("assertAlive", () => { describe("when the node is alive", () => { it("does not warn", () => { const warnSpy = spyOn(console, "warn") const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.assertAlive({}) expect(warnSpy).not.toHaveBeenCalled() }) }) describe("when the node is not alive", () => { it("warns about liveliness", () => { const warnSpy = spyOn(console, "warn") const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.die() node.assertAlive({}) const receivedErrorMessage = warnSpy.mock.calls[0][0].toString() expect(receivedErrorMessage).toBe( "Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'TestModel', Path upon death: '', Subpath: '', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree." ) }) }) }) describe("assertWritable", () => { describe("when the node is not alive", () => { it("throws an error", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.die() expect(() => node.assertWritable({})).toThrow( "[mobx-state-tree] Cannot modify 'TestModel@ [dead]', the object is protected and can only be modified by using an action." ) }) }) describe("when the node is alive and protected", () => { it("throws an error", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) // Nodes are protected by default expect(() => node.assertWritable({})).toThrow( "[mobx-state-tree] Cannot modify 'TestModel@', the object is protected and can only be modified by using an action." ) }) }) describe("when the node is alive and not protected", () => { it("does not throw an error", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) unprotect(node.root.value) expect(() => node.assertWritable({})).not.toThrow() }) }) }) describe("clearParent", () => { it("removes the parent from a node", () => { const parent = Parent.create({ child: { title: "hello" } }) const child = parent.child expect(child).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property child!.$treenode.clearParent() expect(parent.child).toBeUndefined() }) }) describe("createObservableInstance", () => { describe("when the node is still initializing", () => { it("works", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.storedValue).toBeUndefined() expect(node.state).toBe(0) node.createObservableInstance() expect(node.storedValue).toBeDefined() expect(node.state).toBe(2) }) }) if (process.env.NODE_ENV !== "production") { describe("when the node has been initialized", () => { it("does not work", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.createObservableInstance() expect(() => node.createObservableInstance()).toThrow( "[mobx-state-tree] assertion failed: the creation of the observable instance must be done on the initializing phase" ) }) }) } if (process.env.NODE_ENV !== "production") { describe("if the node is dead", () => { it("does not work", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.die() expect(() => node.createObservableInstance()).toThrow( "[mobx-state-tree] assertion failed: the creation of the observable instance must be done on the initializing phase" ) }) }) } }) describe("createObservableInstanceIfNeeded", () => { describe("when the node is still initializing", () => { it("works", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.storedValue).toBeUndefined() expect(node.state).toBe(0) node.createObservableInstanceIfNeeded() expect(node.storedValue).toBeDefined() expect(node.state).toBe(2) }) }) describe("when the node has been initialized", () => { it("does not throw, but an observable instance should be available", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstanceIfNeeded() expect(() => node.createObservableInstanceIfNeeded()).not.toThrow() expect(node.storedValue).toBeDefined() expect(node.state).toBe(2) }) }) if (process.env.NODE_ENV !== "production") { describe("if the node is dead", () => { it("does not work", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.die() expect(() => node.createObservableInstance()).toThrow( "[mobx-state-tree] assertion failed: the creation of the observable instance must be done on the initializing phase" ) }) }) } }) describe("detach", () => { describe("when the node is not alive", () => { it("does throws an error", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.die() expect(() => node.detach()).toThrow("Error while detaching, node is not alive.") }) }) describe("when the node is alive and does not have a parent", () => { it("does not throw an error", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(() => node.detach()).not.toThrow() }) }) describe("when the node is alive and has a parent", () => { it("detaches the node from the parent", () => { const parent = Parent.create({ child: { title: "hello" } }) const child = parent.child expect(child).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property child!.$treenode.detach() expect(parent.child).toBeUndefined() }) }) }) describe("die", () => { describe("if the node is already dead", () => { it("does nothing", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.die() expect(() => node.die()).not.toThrow() }) }) describe("if the node is detaching", () => { it("does nothing", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.detach() expect(() => node.die()).not.toThrow() }) }) describe("if the node is unititalized", () => { it("does not call the onAboutToDie hooks", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.beforeDestroy, hook) node.die() expect(hook).not.toHaveBeenCalled() }) }) describe("if the die method gets past lifecycle checks", () => { it('calls the "aboutToDie" hook', () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.beforeDestroy, hook) node.createObservableInstance() node.die() expect(hook).toHaveBeenCalled() }) it("finalizes the death of its children", () => { const parent = Parent.create({ child: { title: "hello" } }) const child = parent.child expect(child).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property parent.$treenode.die() expect(child!.$treenode.state).toBe(4) }) it("notifies the identifier cache that it has died", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const identifierCache = node.root.identifierCache const identifierCacheNotifySpy = identifierCache ? spyOn(identifierCache, "notifyDied") : jest.fn() node.createObservableInstance() node.die() expect(identifierCacheNotifySpy).toHaveBeenCalledWith(node) }) it("stores the snapshot upon death", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstance() node.die() expect(node.snapshot).toEqual({ title: "hello" }) }) it("sets the subpath upon death", () => { const parent = Parent.create({ child: { title: "hello" } }) const child = parent.child expect(child).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property parent.$treenode.die() expect(child!.$treenode.subpathUponDeath).toBe("child") }) it("sets the path upon death", () => { const parent = Parent.create({ child: { title: "hello" } }) const child = parent.child expect(child).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property parent.$treenode.die() expect(child!.$treenode.pathUponDeath).toBe("/child") }) it("sets its parent to null", () => { const parent = Parent.create({ child: { title: "hello" } }) const child = parent.child expect(child).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property parent.$treenode.die() expect(child!.$treenode.parent).toBeNull() }) it("sets the state to dead", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstance() node.die() expect(node.state).toBe(4) }) }) }) describe("emitPatch", () => { it("emits the patch and a reverse patch", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstance() const patchMock = jest.fn() onPatch(node.storedValue, patchMock) node.emitPatch( { op: "replace", path: "title", value: "world", oldValue: "hello" }, node ) expect(patchMock).toHaveBeenCalledWith( { op: "replace", path: "/title", value: "world" }, { op: "replace", path: "/title", value: "hello" } ) }) it("emits the patch and a reverse patch through its parent", () => { const parent = Parent.create({ child: { title: "hello" } }) const patchMock = jest.fn() onPatch(parent, patchMock) parent.child!.$treenode.emitPatch( { op: "replace", path: "title", value: "world", oldValue: "hello" }, parent.child!.$treenode ) expect(patchMock).toHaveBeenCalledWith( { op: "replace", path: "/child/title", value: "world" }, { op: "replace", path: "/child/title", value: "hello" } ) }) }) describe("finalizeCreation", () => { if (process.env.NODE_ENV !== "production") { describe("if the node is not alive", () => { it("fails", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.die() expect(() => node.finalizeCreation()).toThrow( "assertion failed: cannot finalize the creation of a node that is already dead" ) }) }) } describe("when a node has no parent", () => { it("calls the afterCreationFinalization hook", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.afterCreationFinalization, hook) node.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test node.finalizeCreation() expect(hook).toHaveBeenCalled() }) it('sets the state to "FINALIZED"', () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test node.finalizeCreation() // @ts-expect-error - We're testing the internal state here expect(node.state).toBe(2) }) }) describe("when the node has a parent", () => { describe("but the parent is not yet finalized", () => { it("does not call the afterAttach hook", () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const child = new ObjectNode( TestModel as any, parent, "", {}, { title: "hello" } ) const hook = jest.fn() child.registerHook(Hook.afterCreationFinalization, hook) child.setParent(parent, "child") child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test child.finalizeCreation() expect(hook).not.toHaveBeenCalled() }) it('does not set the state to "FINALIZED"', () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const child = new ObjectNode( TestModel as any, parent, "", {}, { title: "hello" } ) child.setParent(parent, "child") child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test child.finalizeCreation() expect(child.state).toBe(1) }) }) describe("and the parent is finalized", () => { it("calls the afterAttach hook", () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const child = new ObjectNode( TestModel as any, parent, "", {}, { title: "hello" } ) const hook = jest.fn() child.registerHook(Hook.afterCreationFinalization, hook) child.setParent(parent, "child") child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test parent.state = 2 // Force the state to FINALIZED so we don't bail out during the baseFinalizeCreation on child child.finalizeCreation() expect(hook).toHaveBeenCalled() }) it('sets the state to "FINALIZED"', () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const child = new ObjectNode( TestModel as any, parent, "", {}, { title: "hello" } ) child.setParent(parent, "child") child.state = 1 // Force the state to CREATED in the child so we don't bail out in the isAlive check as per the prior test parent.state = 2 // Force the state to FINALIZED so we don't bail out during the baseFinalizeCreation on child child.finalizeCreation() // @ts-expect-error - We're testing the internal state here expect(child.state).toBe(2) }) }) }) describe("when the node has children", () => { it("fires the finalizeCreation hook on the parent", () => { const env = {} const child = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) const parent = new ObjectNode(Parent as any, null, "", env, { child: child.storedValue }) child.setParent(parent, "child") const hook = jest.fn() parent.registerHook(Hook.afterCreationFinalization, hook) child.state = 1 expect(hook).not.toHaveBeenCalled() parent.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test parent.finalizeCreation() expect(hook).toHaveBeenCalled() }) it("fires the afterAttach hook on the child", () => { const env = {} const child = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) const parent = new ObjectNode(Parent as any, null, "", env, { child: child.storedValue }) child.setParent(parent, "child") const c = parent.getChildNode("child") const hook = jest.fn() c.registerHook(Hook.afterAttach, hook) c.state = 1 expect(hook).not.toHaveBeenCalled() parent.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test parent.finalizeCreation() expect(hook).toHaveBeenCalled() }) it("sets both child and parent states to finalized", () => { const env = {} const child = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) const parent = new ObjectNode(Parent as any, null, "", env, { child: child.storedValue }) child.setParent(parent, "child") const c = parent.getChildNode("child") c.state = 1 parent.state = 1 // Force the state to CREATED so we don't bail out in the isAlive check as per the prior test parent.finalizeCreation() // @ts-expect-error - We're testing the internal state here expect(c.state).toBe(2) // @ts-expect-error - We're testing the internal state here expect(parent.state).toBe(2) }) }) }) describe("finalizeDeath", () => { it("does everything die() does without calling aboutToDie()", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.beforeDestroy, hook) node.createObservableInstance() node.finalizeDeath() expect(hook).not.toHaveBeenCalled() expect(node.state).toBe(4) }) }) describe("getChildNode", () => { if (process.env.NODE_ENV !== "production") { describe("when the node is not alive", () => { it("fails", () => { const warnSpy = spyOn(console, "warn") const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.die() node.getChildNode("title") const receivedErrorMessage = warnSpy.mock.calls[0][0].toString() expect(receivedErrorMessage).toBe( "Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'TestModel', Path upon death: '', Subpath: 'title', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree." ) }) }) } describe("when the node is alive", () => { it("returns the child node", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstance() const childNode = node.getChildNode("title") expect(childNode).toBeDefined() expect(childNode.storedValue).toBe("hello") }) }) }) describe("getChildType", () => { it("returns the child type", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const childType = node.getChildType("title") expect(childType).toBe(t.string) }) }) describe("getChildren", () => { describe("when the node is not alive", () => { it("fails", () => { const warnSpy = spyOn(console, "warn") const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.die() node.getChildren() const receivedErrorMessage = warnSpy.mock.calls[0][0].toString() expect(receivedErrorMessage).toBe( "Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree. (Object type: 'TestModel', Path upon death: '', Subpath: '', Action: ''). Either detach nodes first, or don't use objects after removing / replacing them in the tree." ) }) }) describe("when the node is alive and has children", () => { it("returns an array of children", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstance() const children = node.getChildren() expect(children).toBeDefined() expect(children.length).toBe(1) expect(children[0].storedValue).toBe("hello") }) }) }) describe("getReconciliationType", () => { it("returns the complext type used to instantiate the node", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.getReconciliationType()).toBe(TestModel) }) }) describe("getSnapshot", () => { it("returns the snapshot", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.getSnapshot()).toEqual({ title: "hello" }) }) }) describe("hasDisposer", () => { describe("when there are no disposers", () => { it("returns false", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.hasDisposer(() => {})).toBe(false) }) }) describe("when there are disposers", () => { it("returns true", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const disposer = () => {} node.addDisposer(disposer) expect(node.hasDisposer(disposer)).toBe(true) }) }) }) describe("onPatch", () => { it("registers the patch listener", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const listener = jest.fn() node.onPatch(listener) node.createObservableInstance() node.applyPatches([{ op: "replace", path: "/title", value: "world" }]) expect(listener).toHaveBeenCalledWith( { op: "replace", path: "/title", value: "world" }, { op: "replace", path: "/title", value: "hello" } ) }) }) describe("onSnapshot", () => { it("registers the snapshot listener", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const listener = jest.fn() node.onSnapshot(listener) node.createObservableInstance() node.applySnapshot({ title: "world" }) expect(listener).toHaveBeenCalledWith({ title: "world" }) }) }) describe("registerHook", () => { describe("afterCreate", () => { it("works", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.afterCreate, hook) // We call afterCreate during observable instance creation node.createObservableInstance() expect(hook).toHaveBeenCalled() }) }) describe("afterAttach", () => { describe("for a root node", () => { it("does not get called", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) const hook = jest.fn() node.registerHook(Hook.afterAttach, hook) // We call afterAttach during observable instance creation node.createObservableInstance() expect(hook).not.toHaveBeenCalled() }) }) describe("for a non-root node", () => { it("gets called", () => { const env = {} const child = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) const parent = new ObjectNode(Parent as any, null, "", env, { child: child.storedValue }) child.setParent(parent, "child") const c = parent.getChildNode("child") const hook = jest.fn() c.registerHook(Hook.afterAttach, hook) parent.createObservableInstance() expect(hook).toHaveBeenCalled() }) }) }) describe("afterCreationFinalization", () => { it("works", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.afterCreationFinalization, hook) // We call afterCreationFinalization during observable instance creation node.createObservableInstance() expect(hook).toHaveBeenCalled() }) }) describe("beforeDetach", () => { describe("for a root node", () => { it("does not get called", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) const hook = jest.fn() node.registerHook(Hook.beforeDetach, hook) node.createObservableInstance() node.detach() expect(hook).not.toHaveBeenCalled() }) }) describe("for a non-root node", () => { it("gets called", () => { const env = {} const child = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) const parent = new ObjectNode(Parent as any, null, "", env, { child: child.storedValue }) child.setParent(parent, "child") const hook = jest.fn() child.registerHook(Hook.beforeDetach, hook) parent.createObservableInstance() unprotect(parent.storedValue) // In order to detach a child node directly, we need to unprotect the root here child.detach() expect(hook).toHaveBeenCalled() }) }) }) describe("beforeDestroy", () => { it("works", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.beforeDestroy, hook) // We need to create an observable instance before we can destroy it and get the hook to fire node.createObservableInstance() node.die() expect(hook).toHaveBeenCalled() }) }) }) describe("removeChild", () => { describe("for models", () => { it("removes the child by path", () => { const parent = Parent.create({ child: { title: "hello" } }) const child = parent.child expect(child).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property parent.$treenode.removeChild("child") expect(parent.child).toBeUndefined() }) }) describe("for arrays", () => { it("removes the child by index", () => { const parent = TestArray.create([{ title: "hello" }]) expect(parent[0]).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property parent.$treenode.removeChild(0) expect(parent[0]).toBeUndefined() }) }) describe("for maps", () => { it("removes the child by key", () => { const parent = TestMap.create({ hello: { title: "hello" } }) expect(parent.get("hello")).toBeDefined() // We can't directly modify the tree without unprotecting it first unprotect(parent) // The object node is made available through the $treenode property parent.$treenode.removeChild("hello") expect(parent.get("hello")).toBeUndefined() }) }) }) describe("removeDisposer", () => { describe("when a disposer does not exist", () => { it("throws an error", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(() => node.removeDisposer(() => {})).toThrow( "[mobx-state-tree] cannot remove a disposer which was never registered for execution" ) }) }) describe("when a disposer exists", () => { it("removes the disposer", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const disposer = () => {} node.addDisposer(disposer) node.removeDisposer(disposer) expect(node.hasDisposer(disposer)).toBe(false) }) }) }) describe("setParent", () => { describe("if the parent and subpath are unchanged", () => { it("does nothing", () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const node = new ObjectNode( TestModel as any, parent, "child", {}, { title: "hello" } ) node.setParent(parent, "child") expect(node.parent).toBe(parent) expect(node.subpath).toBe("child") }) }) if (process.env.NODE_ENV !== "production") { describe("if there is no subpath", () => { it("throws an error", () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const node = new ObjectNode( TestModel as any, parent, "child", {}, { title: "hello" } ) expect(() => node.setParent(parent, "")).toThrow( "[mobx-state-tree] assertion failed: subpath expected" ) }) }) describe("if there is no new parent", () => { it("throws an error", () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const node = new ObjectNode( TestModel as any, parent, "child", {}, { title: "hello" } ) expect(() => node.setParent(null as any, "child")).toThrow( "[mobx-state-tree] assertion failed: new parent expected" ) }) }) describe("if the node already has a parent", () => { it("throws an error", () => { const parent = new ObjectNode(Parent as any, null, "", {}, {}) const node = new ObjectNode( TestModel as any, parent, "child", {}, { title: "hello" } ) const newParent = new ObjectNode(Parent as any, null, "", {}, {}) expect(() => node.setParent(newParent, "child")).toThrow( "[mobx-state-tree] A node cannot exists twice in the state tree. Failed to add TestModel@/child to path '/child'." ) }) }) describe("if the parent is made to be itself", () => { it("throws an error", () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) expect(() => node.setParent(node, "child")).toThrow( "[mobx-state-tree] A state tree is not allowed to contain itself. Cannot assign TestModel@ to path '/child'" ) }) }) describe("if the parent exists in another state tree in a different environment", () => { it("throws an error", () => { const env1 = {} const env2 = {} const node = new ObjectNode(TestModel as any, null, "", env1, { title: "hello" }) const parent = new ObjectNode(Parent as any, null, "", env2, {}) expect(() => node.setParent(parent, "child")).toThrow( "[mobx-state-tree] A state tree cannot be made part of another state tree as long as their environments are different." ) }) }) } describe("if the parent is different", () => { it("gives the node a new parent", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) node.setParent(parent, "child") expect(node.parent).toBe(parent) }) it("fires the afterAttach hook", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) const hook = jest.fn() node.registerHook(Hook.afterAttach, hook) node.setParent(parent, "child") expect(hook).toHaveBeenCalled() }) }) describe("if the parent is the same but the subpath has changed", () => { it("gives the node a new subpath", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, parent, "", env, { title: "hello" }) node.setParent(parent, "child") expect(node.subpath).toBe("child") }) }) }) describe("toString", () => { it("returns a string representation of the node", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.toString()).toBe("TestModel@") }) }) describe("unbox", () => { // This was probably intended to be used with `null` or `undefined`, but the implementation just checks for falsy values. describe("when given some falsy value", () => { it("returns the value", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.unbox(undefined)).toBeUndefined() // @ts-expect-error - we're testing the behavior of unbox with non-undefined values expect(node.unbox(null as any)).toBe(null) // @ts-expect-error - we're testing the behavior of unbox with non-undefined values expect(node.unbox(false as any)).toBe(false) // @ts-expect-error - we're testing the behavior of unbox with non-undefined values expect(node.unbox(0 as any)).toBe(0) }) }) describe("when given a child node", () => { it("gives back the value", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const childNode = node.getChildNode("title") // @ts-expect-error - we're testing the behavior of unbox with non-undefined values expect(node.unbox(childNode)).toBe("hello") }) }) }) }) describe("properties", () => { describe("_isRunningAciton", () => { // The only time we ever set this to _true is during the operation sin createObservableInstance, so we don't have a great way to test it. For now, just test default value. it("is false by default", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node._isRunningAction).toBe(false) }) }) describe("environmment", () => { it("returns the environment", () => { const env = {} const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) expect(node.environment).toBe(env) }) it("matches by reference", () => { const env = {} // It's using the reference to the object, not just the value of an "empty" object const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.environment).not.toBe(env) }) }) describe("hasSnapshotPostProcessor", () => { it("returns false by default", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.hasSnapshotPostProcessor).toBe(false) }) it("returns false if there is a pre processor and no post processor", () => { const NewType = t.model("NewType", { title: t.string }) const NewType2 = t.snapshotProcessor(NewType, { preProcessor(sn: any) { return { title: sn.title.toUpperCase() } } }) const instance = NewType2.create({ title: "hello" }) const node = instance.$treenode expect(node.hasSnapshotPostProcessor).toBe(false) }) it("returns true if there is a post processor", () => { const NewType = t.model("NewType", { title: t.string }) const NewType2 = t.snapshotProcessor(NewType, { postProcessor(sn: any, node: any) { return { title: sn.title.toUpperCase() } } }) const instance = NewType2.create({ title: "hello" }) const node = instance.$treenode expect(node.hasSnapshotPostProcessor).toBe(true) }) it("returns true if there is a pre processor and a post processor", () => { const NewType = t.model("NewType", { title: t.string }) const NewType2 = t.snapshotProcessor(NewType, { preProcessor(sn: any) { return { title: sn.title.toUpperCase() } }, postProcessor(sn: any, node: any) { return { title: sn.title.toUpperCase() } } }) const instance = NewType2.create({ title: "hello" }) const node = instance.$treenode expect(node.hasSnapshotPostProcessor).toBe(true) }) }) describe("identifier", () => { describe("when the type has no identifier", () => { it("returns null", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.identifier).toBe(null) }) }) describe("when the type has an identifier", () => { it("returns the identifier", () => { const node = new ObjectNode( TestModelWithIdentifier as any, null, "", {}, { id: "1234", title: "hello" } ) expect(node.identifier).toBe("1234") }) }) }) describe("when the type does not have an identifier", () => { it("returns undefined", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.identifierAttribute).toBeUndefined() }) }) describe("when the type has an identifier", () => { it("returns the identifier", () => { const node = new ObjectNode( TestModelWithIdentifier as any, null, "", {}, { id: "1234", title: "hello" } ) expect(node.identifierAttribute).toBe("id") }) }) describe("identifierCache", () => { describe("if the node is the root", () => { it("exists", () => { const Parent = t.model("Parent", { child: TestModel }) const parent = Parent.create({ child: { title: "hello" } }) expect(parent.$treenode.identifierCache).toBeDefined() }) it("keeps track of ids", () => { const Parent = t.model("Parent", { child: TestModelWithIdentifier }) const parent = Parent.create({ child: { id: "1234", title: "hello" } }) const identifierCache = parent.$treenode.identifierCache expect(identifierCache.has(TestModelWithIdentifier, "1234")).toBe(true) expect(identifierCache.has(TestModelWithIdentifier, "aaa")).toBe(false) }) }) describe("if the node is not the root", () => { it("is undefined", () => { const Parent = t.model("Parent", { child: TestModel }) const parent = Parent.create({ child: { title: "hello" } }) expect(parent.child.$treenode.identifierCache).toBeUndefined() }) }) }) describe("isAlive", () => { describe("when the node is dead", () => { it("returns false", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.die() expect(node.isAlive).toBe(false) }) }) describe("when the node is initializing, created, finalized, or detaching", () => { const testCases = [0, 1, 2, 3] testCases.forEach(state => { it(`returns true when the state is ${state}`, () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.state = state expect(node.isAlive).toBe(true) }) }) }) }) describe("isDetaching", () => { describe("when the node is detaching", () => { it("returns true", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.state = 3 expect(node.isDetaching).toBe(true) }) }) describe("when the node is in any other state", () => { const testCases = [0, 1, 2, 4] testCases.forEach(state => { it(`returns false when the state is ${state}`, () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.state = state expect(node.isDetaching).toBe(false) }) }) }) }) describe("isProtected", () => { it("returns true by default", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.isProtected).toBe(true) }) it("returns false if the node is unprotected", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstance() unprotect(node.storedValue) expect(node.isProtected).toBe(false) }) }) describe("isRoot", () => { it("returns true by default", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.isRoot).toBe(true) }) it("returns false when it is made a child of another node", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, null, "", env, { title: "hello" }) node.setParent(parent, "child") expect(node.isRoot).toBe(false) }) }) describe("middlewares", () => { it("returns no middlewares by default", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.middlewares).toBeUndefined() }) it("returns the middlewares", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const middleware = jest.fn() node.addMiddleWare(middleware) expect(node.middlewares).toBeDefined() }) }) describe("nodeId", () => { it("increments every time a node is created", () => { const node1 = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) const node2 = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node1.nodeId).not.toBe(node2.nodeId) }) }) /** * observableIsAlive will follow the isAlive patterns, * it just has a side effect of reporting observation when called */ describe("observableIsAlive", () => { describe("when the node is dead", () => { it("returns false", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.die() expect(node.observableIsAlive).toBe(false) }) }) describe("when the node is initializing, created, finalized, or detaching", () => { const testCases = [0, 1, 2, 3] testCases.forEach(state => { it(`returns true when the state is ${state}`, () => { const node = new ObjectNode( TestModel as any, null, "", {}, { title: "hello" } ) node.state = state expect(node.observableIsAlive).toBe(true) }) }) }) }) describe("parent", () => { it("returns the parent node", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, parent, "", env, { title: "hello" }) expect(node.parent).toBe(parent) }) it("returns null when there is no parent", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.parent).toBe(null) }) }) describe("path", () => { it("returns the provided path", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.path).toBe("") }) }) describe("root", () => { it("returns the node when it is the root", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.root).toBe(node) }) it("returns the root node when it is not the root", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, parent, "", env, { title: "hello" }) expect(node.root).toBe(parent) }) }) describe("snapshot", () => { it("returns the snapshot", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.snapshot).toEqual({ title: "hello" }) }) }) describe("state", () => { // We implicitly test this in many ways through the rest of the spec, so I've just left a simple one here. it("works", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.state).toBe(0) }) }) describe("storedValue", () => { it("is undefined before the observable instance is created", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.storedValue).toBeUndefined() }) it('is the "value" of the observable instance after it is created', () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) node.createObservableInstance() expect(node.storedValue).toBeDefined() // @ts-ignore expect(node.storedValue.title).toBe("hello") }) }) describe("subpath", () => { it('returns "" when the node is the root', () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.subpath).toBe("") }) it("returns the subpath when the node is not the root", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, parent, "child", env, { title: "hello" }) expect(node.subpath).toBe("child") }) }) describe("subpathUponDeath", () => { it("remembers the subpath upon death", () => { const env = {} const parent = new ObjectNode(Parent as any, null, "", env, {}) const node = new ObjectNode(TestModel as any, parent, "child", env, { title: "hello" }) node.die() expect(node.subpathUponDeath).toBe("child") parent.die() expect(parent.subpathUponDeath).toBe("") }) }) describe("type", () => { it("returns the given type of the objectnode", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) expect(node.type).toBe(TestModel as any) }) }) describe("value", () => { it("returns the value as returned by the given type", () => { const node = new ObjectNode(TestModel as any, null, "", {}, { title: "hello" }) // @ts-ignore expect(node.value.title).toBe("hello") }) }) }) }) ================================================ FILE: __tests__/core/object.test.ts ================================================ import { destroy, detach, onSnapshot, onPatch, onAction, applyPatch, applyAction, applySnapshot, getSnapshot, unprotect, types, setLivelinessChecking, getParent, SnapshotOut, IJsonPatch, ISerializedActionCall, isAlive, cast, resolveIdentifier } from "../../src" import { autorun, reaction, observable, configure, getDebugName } from "mobx" import { MstError } from "../../src/internal" import { expect, test } from "bun:test" const createTestFactories = () => { const Factory = types .model({ to: "world" }) .actions(self => { function setTo(to: string) { self.to = to } return { setTo } }) const ComputedFactory = types .model({ width: 100, height: 200 }) .views(self => ({ get area() { return self.width * self.height } })) const ComputedFactory2 = types .model({ props: types.map(types.number) }) .views(self => ({ get area() { return self.props.get("width")! * self.props.get("height")! } })) .actions(self => { function setWidth(value: number) { self.props.set("width", value) } function setHeight(value: number) { self.props.set("height", value) } return { setWidth, setHeight } }) const BoxFactory = types.model({ width: 0, height: 0 }) const ColorFactory = types.model({ color: "#FFFFFF" }) return { Factory, ComputedFactory, ComputedFactory2, BoxFactory, ColorFactory } } const createFactoryWithChildren = () => { const File = types .model("File", { name: types.string }) .actions(self => ({ rename(value: string) { self.name = value } })) const Folder = types .model("Folder", { name: types.string, files: types.array(File) }) .actions(self => ({ rename(value: string) { self.name = value } })) return Folder } // === FACTORY TESTS === test("it should create a factory", () => { const { Factory } = createTestFactories() const instance = Factory.create() const snapshot = getSnapshot(instance) expect(snapshot).toEqual({ to: "world" }) expect(getSnapshot(Factory.create())).toEqual({ to: "world" }) // toJSON is there as shortcut for getSnapshot(), primarily for debugging convenience expect(Factory.create().toString()).toEqual("AnonymousModel@") }) test("it should restore the state from the snapshot", () => { const { Factory } = createTestFactories() expect(getSnapshot(Factory.create({ to: "universe" }))).toEqual({ to: "universe" }) }) // === SNAPSHOT TESTS === test("it should emit snapshots", () => { const { Factory } = createTestFactories() const doc = Factory.create() unprotect(doc) let snapshots: SnapshotOut[] = [] onSnapshot(doc, snapshot => snapshots.push(snapshot)) doc.to = "universe" expect(snapshots).toEqual([{ to: "universe" }]) }) test("it should emit snapshots for children", () => { const Factory = createFactoryWithChildren() const folder = Factory.create({ name: "Photos to sort", files: [ { name: "Photo1" }, { name: "Photo2" } ] }) let snapshotsP: SnapshotOut[] = [] let snapshotsC: SnapshotOut<(typeof folder.files)[0]>[] = [] onSnapshot(folder, snapshot => snapshotsP.push(snapshot)) folder.rename("Vacation photos") expect(snapshotsP[0]).toEqual({ name: "Vacation photos", files: [{ name: "Photo1" }, { name: "Photo2" }] }) onSnapshot(folder.files[0], snapshot => snapshotsC.push(snapshot)) folder.files[0].rename("01-arrival") expect(snapshotsP[1]).toEqual({ name: "Vacation photos", files: [{ name: "01-arrival" }, { name: "Photo2" }] }) expect(snapshotsC[0]).toEqual({ name: "01-arrival" }) folder.files[1].rename("02-hotel") expect(snapshotsP[2]).toEqual({ name: "Vacation photos", files: [{ name: "01-arrival" }, { name: "02-hotel" }] }) expect(snapshotsP.length).toBe(3) expect(snapshotsC.length).toBe(1) }) test("it should apply snapshots", () => { const { Factory } = createTestFactories() const doc = Factory.create() applySnapshot(doc, { to: "universe" }) expect(getSnapshot(doc)).toEqual({ to: "universe" }) }) test("it should apply and accept null value for types.maybe(complexType)", () => { const Item = types.model("Item", { value: types.string }) const Model = types.model("Model", { item: types.maybe(Item) }) const myModel = Model.create() applySnapshot(myModel, { item: { value: "something" } }) applySnapshot(myModel, { item: undefined }) expect(getSnapshot(myModel)).toEqual({ item: undefined }) }) test("it should apply and accept null value for types.maybeNull(complexType)", () => { const Item = types.model("Item", { value: types.string }) const Model = types.model("Model", { item: types.maybeNull(Item) }) const myModel = Model.create() applySnapshot(myModel, { item: { value: "something" } }) applySnapshot(myModel, { item: null }) expect(getSnapshot(myModel)).toEqual({ item: null }) }) test("it should return a snapshot", () => { const { Factory } = createTestFactories() const doc = Factory.create() expect(getSnapshot(doc)).toEqual({ to: "world" }) }) // === PATCHES TESTS === test("it should emit patches", () => { const { Factory } = createTestFactories() const doc = Factory.create() unprotect(doc) let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) doc.to = "universe" expect(patches).toEqual([{ op: "replace", path: "/to", value: "universe" }]) }) test("it should apply a patch", () => { const { Factory } = createTestFactories() const doc = Factory.create() applyPatch(doc, { op: "replace", path: "/to", value: "universe" }) expect(getSnapshot(doc)).toEqual({ to: "universe" }) }) test("it should apply patches", () => { const { Factory } = createTestFactories() const doc = Factory.create() applyPatch(doc, [ { op: "replace", path: "/to", value: "mars" }, { op: "replace", path: "/to", value: "universe" } ]) expect(getSnapshot(doc)).toEqual({ to: "universe" }) }) test("it should stop listening to patches patches", () => { const { Factory } = createTestFactories() const doc = Factory.create() unprotect(doc) let patches: IJsonPatch[] = [] let disposer = onPatch(doc, patch => patches.push(patch)) doc.to = "universe" disposer() doc.to = "mweststrate" expect(patches).toEqual([{ op: "replace", path: "/to", value: "universe" }]) }) // === ACTIONS TESTS === test("it should call actions correctly", () => { const { Factory } = createTestFactories() const doc = Factory.create() doc.setTo("universe") expect(getSnapshot(doc)).toEqual({ to: "universe" }) }) test("it should emit action calls", () => { const { Factory } = createTestFactories() const doc = Factory.create() let actions: ISerializedActionCall[] = [] onAction(doc, action => actions.push(action)) doc.setTo("universe") expect(actions).toEqual([{ name: "setTo", path: "", args: ["universe"] }]) }) test("it should apply action call", () => { const { Factory } = createTestFactories() const doc = Factory.create() applyAction(doc, { name: "setTo", path: "", args: ["universe"] }) expect(getSnapshot(doc)).toEqual({ to: "universe" }) }) test("it should apply actions calls", () => { const { Factory } = createTestFactories() const doc = Factory.create() applyAction(doc, [ { name: "setTo", path: "", args: ["mars"] }, { name: "setTo", path: "", args: ["universe"] } ]) expect(getSnapshot(doc)).toEqual({ to: "universe" }) }) // === COMPUTED VALUES === test("it should have computed properties", () => { const { ComputedFactory } = createTestFactories() const doc = ComputedFactory.create() unprotect(doc) doc.width = 3 doc.height = 2 expect(doc.area).toEqual(6) }) test("it should throw if a replaced object is read or written to", () => { const Todo = types .model("Todo", { title: "test", arr: types.array(types.string), map: types.map(types.string), sub: types.optional( types .model("Sub", { title: "test2" }) .actions(self => ({ fn2() {} })), {} ) }) .actions(self => ({ fn() { self.sub.fn2() } })) const Store = types.model("Store", { todo: Todo }) const data = { title: "alive", arr: ["arr0"], map: { mapkey0: "mapval0" }, sub: { title: "title" } } const s = Store.create({ todo: { ...data, title: "dead" } }) unprotect(s) const deadArr = s.todo.arr s.todo.arr = cast(data.arr) const deadMap = s.todo.map s.todo.map = cast(data.map) const deadSub = s.todo.sub s.todo.sub = cast(data.sub) const deadTodo = s.todo s.todo = Todo.create(data) expect(s.todo.title).toBe("alive") setLivelinessChecking("error") function getError(obj: any, path: string, subpath: string, action: string) { return `You are trying to read or write to an object that is no longer part of a state tree. (Object type: '${getDebugName( obj )}', Path upon death: '${path}', Subpath: '${subpath}', Action: '${action}'). Either detach nodes first, or don't use objects after removing / replacing them in the tree.` } // dead todo expect(() => { deadTodo.fn() }).toThrow(getError(deadTodo, "/todo", "", "/todo.fn()")) expect(() => { // tslint:disable-next-line:no-unused-expression deadTodo.title }).toThrow(getError(deadTodo, "/todo", "title", "")) expect(() => { deadTodo.title = "5" }).toThrow(getError(deadTodo, "/todo", "title", "")) expect(() => { // tslint:disable-next-line:no-unused-expression deadTodo.arr[0] }).toThrow(getError(deadTodo, "/todo", "arr", "")) expect(() => { deadTodo.arr.push("arr1") }).toThrow(getError(deadTodo, "/todo", "arr", "")) expect(() => { deadTodo.map.get("mapkey0") }).toThrow(getError(deadTodo, "/todo", "map", "")) expect(() => { deadTodo.map.set("mapkey1", "val") }).toThrow(getError(deadTodo, "/todo", "map", "")) expect(() => { deadTodo.sub.fn2() }).toThrow(getError(deadTodo, "/todo", "sub", "")) expect(() => { // tslint:disable-next-line:no-unused-expression deadTodo.sub.title }).toThrow(getError(deadTodo, "/todo", "sub", "")) expect(() => { deadTodo.sub.title = "hi" }).toThrow(getError(deadTodo, "/todo", "sub", "")) // dead array expect(() => { // tslint:disable-next-line:no-unused-expression deadArr[0] }).toThrow(getError(deadArr, "/todo/arr", "0", "")) expect(() => { deadArr[0] = "hi" }).toThrow(getError(deadArr, "/todo/arr", "0", "")) expect(() => { deadArr.push("hi") }).toThrow(getError(deadArr, "/todo/arr", "1", "")) // dead map expect(() => { deadMap.get("mapkey0") }).toThrow(getError(deadMap, "/todo/map", "mapkey0", "")) expect(() => { deadMap.set("mapkey0", "val") }).toThrow(getError(deadMap, "/todo/map", "mapkey0", "")) // dead subobj expect(() => { deadSub.fn2() }).toThrow(getError(deadSub, "/todo/sub", "", "/todo/sub.fn2()")) expect(() => { // tslint:disable-next-line:no-unused-expression deadSub.title }).toThrow(getError(deadSub, "/todo/sub", "title", "")) expect(() => { deadSub.title = "ho" }).toThrow(getError(deadSub, "/todo/sub", "title", "")) }) test("it should warn if a replaced object is read or written to", () => { const Todo = types .model("Todo", { title: "test" }) .actions(self => { function fn() {} return { fn } }) const Store = types.model("Store", { todo: Todo }) const s = Store.create({ todo: { title: "3" } }) unprotect(s) const todo = s.todo s.todo = Todo.create({ title: "4" }) expect(s.todo.title).toBe("4") // try reading old todo setLivelinessChecking("error") const error = "You are trying to read or write to an object that is no longer part of a state tree" expect(() => todo.fn()).toThrow(error) expect(() => todo.title).toThrow(error) unprotect(todo) expect(() => { todo.title = "5" }).toThrow(error) }) // === COMPOSE FACTORY === test("it should compose factories", () => { const { BoxFactory, ColorFactory } = createTestFactories() const ComposedFactory = types.compose(BoxFactory, ColorFactory) expect(getSnapshot(ComposedFactory.create())).toEqual({ width: 0, height: 0, color: "#FFFFFF" }) }) test("it should compose factories with computed properties", () => { const { ComputedFactory2, ColorFactory } = createTestFactories() const ComposedFactory = types.compose(ColorFactory, ComputedFactory2) const store = ComposedFactory.create({ props: { width: 100, height: 200 } }) expect(getSnapshot(store)).toEqual({ props: { width: 100, height: 200 }, color: "#FFFFFF" }) expect(store.area).toBe(20000) expect(typeof store.setWidth).toBe("function") expect(typeof store.setHeight).toBe("function") }) test("it should compose multiple types with computed properties", () => { const { ComputedFactory2, ColorFactory } = createTestFactories() const ComposedFactory = types.compose(ColorFactory, ComputedFactory2) const store = ComposedFactory.create({ props: { width: 100, height: 200 } }) expect(getSnapshot(store)).toEqual({ props: { width: 100, height: 200 }, color: "#FFFFFF" }) expect(store.area).toBe(20000) expect(typeof store.setWidth).toBe("function") expect(typeof store.setHeight).toBe("function") }) test("methods get overridden by compose", () => { const A = types .model({ count: types.optional(types.number, 0) }) .actions(self => { function increment() { self.count += 1 } return { increment } }) const B = A.actions(self => ({ increment() { self.count += 10 } })) const store = B.create() expect(getSnapshot(store)).toEqual({ count: 0 }) expect(store.count).toBe(0) store.increment() expect(store.count).toBe(10) }) test("compose should add new props", () => { const A = types.model({ count: types.optional(types.number, 0) }) const B = A.props({ called: types.optional(types.number, 0) }) const store = B.create() expect(getSnapshot(store)).toEqual({ count: 0, called: 0 }) expect(store.count).toBe(0) }) test("models should expose their actions to be used in a composable way", () => { const A = types .model({ count: types.optional(types.number, 0) }) .actions(self => { function increment() { self.count += 1 } return { increment } }) const B = A.props({ called: types.optional(types.number, 0) }).actions(self => { const baseIncrement = self.increment return { increment() { baseIncrement() self.called += 1 } } }) const store = B.create() expect(getSnapshot(store)).toEqual({ count: 0, called: 0 }) expect(store.count).toBe(0) store.increment() expect(store.count).toBe(1) expect(store.called).toBe(1) }) test("compose should be overwrite", () => { const A = types .model({ name: "", alias: "" }) .views(self => ({ get displayName() { return self.alias || self.name } })) const B = A.props({ type: "" }).views(self => ({ get displayName() { return self.alias || self.name + self.type } })) const storeA = A.create({ name: "nameA", alias: "aliasA" }) const storeB = B.create({ name: "nameB", alias: "aliasB", type: "typeB" }) const storeC = B.create({ name: "nameC", type: "typeC" }) expect(storeA.displayName).toBe("aliasA") expect(storeB.displayName).toBe("aliasB") expect(storeC.displayName).toBe("nameCtypeC") }) // === TYPE CHECKS === test("it should check the type correctly", () => { const { Factory } = createTestFactories() const doc = Factory.create() expect(Factory.is(doc)).toEqual(true) expect(Factory.is([])).toEqual(false) expect(Factory.is({})).toEqual(true) expect(Factory.is({ to: "mars" })).toEqual(true) expect(Factory.is({ wrongKey: true })).toEqual(true) expect(Factory.is({ to: 3 })).toEqual(false) }) if (process.env.NODE_ENV !== "production") { test("complex map / array values are optional by default", () => { expect( types .model({ todo: types.model({}) }) .is({}) ).toBe(false) expect(() => types .model({ todo: types.model({}) }) .create({} as any) ).toThrow() expect( types .model({ todo: types.array(types.string) }) .is({}) ).toBe(true) // TBD: or true? expect( getSnapshot( types .model({ todo: types.array(types.string) }) .create({}) ) ).toEqual({ todo: [] }) expect( types .model({ todo: types.map(types.string) }) .is({}) ).toBe(true) expect( getSnapshot( types .model({ todo: types.map(types.string) }) .create({}) ) ).toEqual({ todo: {} }) }) } // === VIEW FUNCTIONS === test("view functions should be tracked", () => { const model = types .model({ x: 3 }) .views(self => ({ doubler() { return self.x * 2 } })) .create() unprotect(model) const values: number[] = [] const d = autorun(() => { values.push(model.doubler()) }) model.x = 7 expect(values).toEqual([6, 14]) }) test("view functions should not be allowed to change state", () => { const model = types .model({ x: 3 }) .views(self => ({ doubler() { self.x *= 2 } })) .actions(self => { function anotherDoubler() { self.x *= 2 } return { anotherDoubler } }) .create() expect(() => model.doubler()).toThrow() model.anotherDoubler() expect(model.x).toBe(6) }) test("it should consider primitives as proposed defaults", () => { const now = new Date() const Todo = types.model({ id: 0, name: "Hello world", done: false, createdAt: now }) const doc = Todo.create() expect(getSnapshot(doc)).toEqual({ id: 0, name: "Hello world", done: false, createdAt: now.getTime() }) }) test("it should throw if a non-primitive value is provided and no default can be created", () => { expect(() => { types.model({ complex: { a: 1, b: 2 } as any }) }).toThrow() }) if (process.env.NODE_ENV !== "production") { test("it should not be possible to remove a node from a parent if it is required, see ", () => { const A = types.model("A", { x: 3 }) const B = types.model("B", { a: A }) const b = B.create({ a: { x: 7 } }) unprotect(b) expect(() => { detach(b.a) }).toThrow(/Error while converting `undefined` to `A`/) expect(() => { destroy(b.a) }).toThrow(/Error while converting `undefined` to `A`/) }) test("it should be possible to remove a node from a parent if it is defined as type maybe ", () => { const A = types.model("A", { x: 3 }) const B = types.model("B", { a: types.maybe(A) }) const b = B.create({ a: { x: 7 } }) unprotect(b) expect(() => { const a = b.a! detach(a) destroy(a) }).not.toThrow() expect(b.a).toBeUndefined() expect(getSnapshot(b).a).toBeUndefined() }) test("it should be possible to remove a node from a parent if it is defined as type maybeNull ", () => { const A = types.model("A", { x: 3 }) const B = types.model("B", { a: types.maybeNull(A) }) const b = B.create({ a: { x: 7 } }) unprotect(b) expect(() => { const a = b.a! detach(a) destroy(a) }).not.toThrow() expect(b.a).toBe(null) expect(getSnapshot(b).a).toBe(null) }) } test("it should be possible to share states between views and actions using enhance", () => { const A = types.model({}).extend(self => { const localState = observable.box(3) return { views: { get x() { return localState.get() } }, actions: { setX(value: number) { localState.set(value) } } } }) let x = 0 let a = A.create() const d = reaction( () => a.x, v => { x = v } ) a.setX(7) expect(a.x).toBe(7) expect(x).toBe(7) d() }) test("It should throw if any other key is returned from extend", () => { const A = types.model({}).extend(() => ({ stuff() {} }) as any) expect(() => A.create()).toThrow(/stuff/) }) test("782, TS + compose", () => { const User = types.model("User", { id: types.identifier, name: types.maybe(types.string), avatar: types.maybe(types.string) }) const user = User.create({ id: "someId" }) }) test("961 - model creating should not change snapshot", () => { const M = types.model({ foo: 1 }) const o = {} const m = M.create(o) expect(o).toEqual({}) expect(getSnapshot(m)).toEqual({ foo: 1 }) }) if (process.env.NODE_ENV === "development") test("beautiful errors", () => { expect(() => { types.model("User", { x: (types.identifier as any)() }) }).toThrow("types.identifier is not a function") expect(() => { types.model("User", { x: { bla: true } as any }) }).toThrow( "Invalid type definition for property 'x', it looks like you passed an object. Try passing another model type or a types.frozen" ) expect(() => { types.model("User", { x: function () {} as any }) }).toThrow( "Invalid type definition for property 'x', it looks like you passed a function. Did you forget to invoke it, or did you intend to declare a view / action?" ) }) test("#967 - changing values in afterCreate/afterAttach when node is instantiated from view", () => { const Answer = types .model("Answer", { title: types.string, selected: false }) .actions(self => ({ toggle() { self.selected = !self.selected } })) const Question = types .model("Question", { title: types.string, answers: types.array(Answer) }) .views(self => ({ get brokenView() { // this should not be allowed // MWE: disabled, MobX 6 no longer forbids this // expect(() => { // self.answers[0].toggle() // }).toThrow() return 0 } })) .actions(self => ({ afterCreate() { // we should allow changes even when inside a computed property when done inside afterCreate/afterAttach self.answers[0].toggle() // but not further computed changes expect(self.brokenView).toBe(0) }, afterAttach() { // we should allow changes even when inside a computed property when done inside afterCreate/afterAttach self.answers[0].toggle() expect(self.brokenView).toBe(0) } })) const Product = types .model("Product", { questions: types.array(Question) }) .views(self => ({ get selectedAnswers() { const result = [] for (const question of self.questions) { result.push(question.answers.find(a => a.selected)) } return result } })) const product = Product.create({ questions: [ { title: "Q 0", answers: [{ title: "A 0.0" }, { title: "A 0.1" }] }, { title: "Q 1", answers: [{ title: "A 1.0" }, { title: "A 1.1" }] } ] }) // tslint:disable-next-line:no-unused-expression product.selectedAnswers }) test("#993-1 - after attach should have a parent when accesing a reference directly", () => { const L4 = types .model("Todo", { id: types.identifier, finished: false }) .actions(self => ({ afterAttach() { expect(getParent(self)).toBeTruthy() } })) const L3 = types.model({ l4: L4 }).actions(self => ({ afterAttach() { expect(getParent(self)).toBeTruthy() } })) const L2 = types .model({ l3: L3 }) .actions(self => ({ afterAttach() { expect(getParent(self)).toBeTruthy() } })) const L1 = types .model({ l2: L2, selected: types.reference(L4) }) .actions(self => ({ afterAttach() { throw new MstError("should never be called") } })) const createL1 = () => L1.create({ l2: { l3: { l4: { id: "11124091-11c1-4dda-b2ed-7dd6323491a5" } } }, selected: "11124091-11c1-4dda-b2ed-7dd6323491a5" }) // test 1, real child first { const l1 = createL1() const a = l1.l2.l3.l4 const b = l1.selected } // test 2, reference first { const l1 = createL1() const a = l1.selected const b = l1.l2.l3.l4 } }) test("#993-2 - references should have a parent even when the parent has not been accessed before", () => { const events: string[] = [] const L4 = types .model("Todo", { id: types.identifier, finished: false }) .actions(self => ({ toggle() { self.finished = !self.finished }, afterCreate() { events.push("l4-ac") }, afterAttach() { events.push("l4-at") } })) const L3 = types.model({ l4: L4 }).actions(self => ({ afterCreate() { events.push("l3-ac") }, afterAttach() { events.push("l3-at") } })) const L2 = types .model({ l3: L3 }) .actions(self => ({ afterCreate() { events.push("l2-ac") }, afterAttach() { events.push("l2-at") } })) const L1 = types .model({ l2: L2, selected: types.reference(L4) }) .actions(self => ({ afterCreate() { events.push("l1-ac") }, afterAttach() { events.push("l1-at") } })) const createL1 = () => L1.create({ l2: { l3: { l4: { id: "11124091-11c1-4dda-b2ed-7dd6323491a5" } } }, selected: "11124091-11c1-4dda-b2ed-7dd6323491a5" }) const expectedEvents = [ "l1-ac", "l2-ac", "l2-at", "l3-ac", "l3-at", "l4-ac", "l4-at", "onSnapshot", "-", "onSnapshot" ] // test 1, real child first { const l1 = createL1() onSnapshot(l1, () => { events.push("onSnapshot") }) l1.l2.l3.l4.toggle() events.push("-") l1.selected.toggle() expect(events).toEqual(expectedEvents) } const expectedEvents2 = [ "l1-ac", "l4-ac", "l3-ac", "l2-ac", "l2-at", "l3-at", "l4-at", "onSnapshot", "-", "onSnapshot" ] // test 2, reference first // the order of hooks is different but they are all called events.length = 0 { const l1 = createL1() onSnapshot(l1, () => { events.push("onSnapshot") }) l1.selected.toggle() events.push("-") l1.l2.l3.l4.toggle() expect(events).toEqual(expectedEvents2) } // test 3, reference get parent should be available from the beginning and all the way to the root { const rootL1 = createL1() const l4 = rootL1.selected const l3 = getParent(l4) expect(l3).toBeTruthy() const l2 = getParent(l3) expect(l2).toBeTruthy() const l1 = getParent(l2) expect(l1).toBeTruthy() expect(l1).toBe(rootL1) expect(l2).toBe(rootL1.l2) expect(l3).toBe(rootL1.l2.l3) expect(l4).toBe(rootL1.l2.l3.l4) } }) test("it should emit patches when applySnapshot is used", () => { const { Factory } = createTestFactories() const doc = Factory.create() let patches: IJsonPatch[] = [] onPatch(doc, patch => patches.push(patch)) applySnapshot(doc, { ...getSnapshot(doc), to: "universe" }) expect(patches).toEqual([{ op: "replace", path: "/to", value: "universe" }]) }) test("isAlive must be reactive", () => { const Todo = types.model({ text: types.string }) const TodoStore = types.model({ todos: types.array(Todo), todo: types.maybe(Todo) }) const store = TodoStore.create({ todos: [{ text: "1" }, { text: "2" }], todo: { text: "3" } }) unprotect(store) const t1 = store.todos[0]! const t2 = store.todos[1]! const t3 = store.todo! let calls = 0 const r1 = reaction( () => isAlive(t1), v => { expect(v).toBe(false) calls++ } ) const r2 = reaction( () => isAlive(t2), v => { expect(v).toBe(false) calls++ } ) const r3 = reaction( () => isAlive(t3), v => { expect(v).toBe(false) calls++ } ) try { store.todos = cast([]) store.todo = undefined expect(calls).toBe(3) } finally { r1() r2() r3() } }) test("#1112 - identifier cache should be cleared for unaccessed wrapped objects", () => { const mock1 = [ { id: "1", name: "Kate" }, { id: "2", name: "John" } ] const mock2 = [ { id: "3", name: "Andrew" }, { id: "2", name: "John" } ] const mock1_2 = mock1.map((i, index) => ({ text: `Text${index}`, entity: i })) const mock2_2 = mock2.map((i, index) => ({ text: `Text${index}`, entity: i })) const Entity = types.model({ id: types.identifier, name: types.string }) const Wrapper = types.model({ text: types.string, entity: Entity }) const Store = types .model({ list: types.optional(types.array(Wrapper), []), selectedId: 2 }) .views(self => ({ get selectedEntity() { return resolveIdentifier(Entity, self, self.selectedId) } })) const store = Store.create() unprotect(store) store.list.replace(mock1_2) store.list.replace(mock2_2) expect(store.selectedEntity!.id).toBe("2") }) test("#1173 - detaching a model should not screw it", () => { const AM = types.model({ x: 5 }) const Store = types.model({ item: types.maybe(AM) }) const s = Store.create({ item: { x: 6 } }) const n0 = s.item unprotect(s) const detachedItem = detach(s.item!) expect(s.item).not.toBe(detachedItem) expect(s.item).toBeUndefined() expect(detachedItem.x).toBe(6) expect(detachedItem).toBe(n0!) }) test("#1702 - should not throw with useProxies: 'ifavailable'", () => { configure({ useProxies: "ifavailable" }) const M = types.model({ x: 5 }).views(self => ({ get y() { return self.x } })) expect(() => { M.create({}) }).not.toThrow() }) ================================================ FILE: __tests__/core/optimizations.test.ts ================================================ import { getSnapshot, applySnapshot, unprotect, types } from "../../src" import { expect, test } from "bun:test" test("it should avoid processing patch if is exactly the current one in applySnapshot", () => { const Model = types.model({ a: types.number, b: types.string }) const store = Model.create({ a: 1, b: "hello" }) const snapshot = getSnapshot(store) applySnapshot(store, snapshot) expect(getSnapshot(store)).toBe(snapshot) // no new snapshot emitted }) test("it should avoid processing patch if is exactly the current one in reconcile", () => { const Model = types.model({ a: types.number, b: types.string }) const RootModel = types.model({ a: Model }) const store = RootModel.create({ a: { a: 1, b: "hello" } }) unprotect(store) // NOTE: snapshots are not equal after property access anymore, // so we test initial and actual ones separately const snapshot = getSnapshot(store) expect(getSnapshot(store)).toEqual(snapshot) store.a = snapshot.a // check whether reconciliation works on initial values expect(getSnapshot(store)).toEqual(snapshot) // access property to initialize observable instance expect(getSnapshot(store.a)).toEqual(snapshot.a) // check whether initializing instance does not cause snapshot invalidation const actualSnapshot = getSnapshot(store) expect(actualSnapshot.a).toBe(snapshot.a) }) ================================================ FILE: __tests__/core/optional-extension.test.ts ================================================ import { getSnapshot, types, unprotect } from "../../src" import { describe, expect, test } from "bun:test" describe("null as default", () => { describe("basic tests", () => { const M = types.model({ x: types.optional(types.number, 1, [null]), y: types.optional(types.number, () => 2, [null]) }) test("with optional values, then assigned values", () => { const m = M.create({ x: null, y: null }) unprotect(m) expect(m.x).toBe(1) expect(m.y).toBe(2) expect(getSnapshot(m)).toEqual({ x: 1, y: 2 }) m.x = 10 m.y = 20 expect(m.x).toBe(10) expect(m.y).toBe(20) expect(getSnapshot(m)).toEqual({ x: 10, y: 20 }) }) test("with given values, then assigned optional values", () => { const m = M.create({ x: 10, y: 20 }) unprotect(m) expect(m.x).toBe(10) expect(m.y).toBe(20) expect(getSnapshot(m)).toEqual({ x: 10, y: 20 }) m.x = null as any m.y = null as any expect(m.x).toBe(1) expect(m.y).toBe(2) expect(getSnapshot(m)).toEqual({ x: 1, y: 2 }) }) }) test("when the underlying type accepts undefined it should be ok", () => { const M = types.model({ a: types.optional(types.union(types.undefined, types.number), undefined, [null]), b: types.optional(types.union(types.undefined, types.number), 5, [null]) }) { const m = M.create({ a: null, b: null }) expect(m.a).toBeUndefined() expect(m.b).toBe(5) expect(getSnapshot(m)).toEqual({ a: undefined, b: 5 }) } { const m = M.create({ a: 10, b: 20 }) expect(m.a).toBe(10) expect(m.b).toBe(20) expect(getSnapshot(m)).toEqual({ a: 10, b: 20 }) } { const m = M.create({ a: undefined, b: undefined }) expect(m.a).toBeUndefined() expect(m.b).toBeUndefined() expect(getSnapshot(m)).toEqual({ a: undefined, b: undefined }) } }) test("when the underlying type does not accept undefined, then undefined should throw", () => { const M = types.model({ a: types.optional(types.number, 5, [null]), b: types.optional(types.number, 6, [null]) }) { const m = M.create({ a: null, b: null }) expect(m.a).toBe(5) expect(m.b).toBe(6) } if (process.env.NODE_ENV !== "production") { expect(() => { M.create({ a: null, b: undefined as any // undefined is not valid }) }).toThrow("value `undefined` is not assignable to type: `number`") expect(() => { M.create({ a: null // b: null missing, but should be there } as any) }).toThrow("value `undefined` is not assignable to type: `number`") } }) }) describe("'empty' or false as default", () => { describe("basic tests", () => { const M = types.model({ x: types.optional(types.number, 1, ["empty", false]), y: types.optional(types.number, () => 2, ["empty", false]) }) test("with optional values, then assigned values", () => { const m = M.create({ x: "empty", y: false }) unprotect(m) expect(m.x).toBe(1) expect(m.y).toBe(2) expect(getSnapshot(m)).toEqual({ x: 1, y: 2 }) m.x = 10 m.y = 20 expect(m.x).toBe(10) expect(m.y).toBe(20) expect(getSnapshot(m)).toEqual({ x: 10, y: 20 }) }) test("with given values, then assigned 'empty'", () => { const m = M.create({ x: 10, y: 20 }) unprotect(m) expect(m.x).toBe(10) expect(m.y).toBe(20) expect(getSnapshot(m)).toEqual({ x: 10, y: 20 }) m.x = "empty" as any m.y = false as any expect(m.x).toBe(1) expect(m.y).toBe(2) expect(getSnapshot(m)).toEqual({ x: 1, y: 2 }) }) }) test("when the underlying type accepts undefined it should be ok", () => { const M = types.model({ a: types.optional(types.union(types.undefined, types.number), undefined, [ "empty", false ]), b: types.optional(types.union(types.undefined, types.number), 5, ["empty", false]) }) { const m = M.create({ a: "empty", b: false }) expect(m.a).toBeUndefined() expect(m.b).toBe(5) expect(getSnapshot(m)).toEqual({ a: undefined, b: 5 }) } { const m = M.create({ a: 10, b: 20 }) expect(m.a).toBe(10) expect(m.b).toBe(20) expect(getSnapshot(m)).toEqual({ a: 10, b: 20 }) } { const m = M.create({ a: undefined, b: undefined }) expect(m.a).toBeUndefined() expect(m.b).toBeUndefined() expect(getSnapshot(m)).toEqual({ a: undefined, b: undefined }) } }) test("when the underlying type does not accept undefined, then undefined should throw", () => { const M = types.model({ a: types.optional(types.number, 5, ["empty", false]), b: types.optional(types.number, 6, ["empty", false]) }) { const m = M.create({ a: "empty", b: false }) expect(m.a).toBe(5) expect(m.b).toBe(6) } if (process.env.NODE_ENV !== "production") { expect(() => { M.create({ a: undefined as any, b: undefined as any }) }).toThrow("value `undefined` is not assignable to type: `number`") } }) }) test("cached snapshots should be ok when using default values", () => { const M = types.model({ x: 5, y: 6 }) const Store = types.model({ deep: types.model({ a: types.optional(types.undefined, undefined), b: types.optional(types.undefined, undefined, ["empty"]), c: types.optional(types.number, 5), d: types.optional(types.number, 5, ["empty"]), a2: types.optional(types.undefined, () => undefined), b2: types.optional(types.undefined, () => undefined, ["empty"]), c2: types.optional(types.number, () => 5), d2: types.optional(types.number, () => 5, ["empty"]), a3: types.optional(M, { y: 20 }), b3: types.optional(M, { y: 20 }, ["empty"]), c3: types.optional(M, () => M.create({ y: 20 })), d3: types.optional(M, () => M.create({ y: 20 }), ["empty"]), e3: types.optional(M, () => ({ y: 20 })), f3: types.optional(M, () => ({ y: 20 }), ["empty"]) }) }) const s = Store.create({ deep: { b: "empty", d: "empty", b2: "empty", d2: "empty", b3: "empty", d3: "empty", f3: "empty" } }) expect(getSnapshot(s)).toEqual({ deep: { a: undefined, b: undefined, c: 5, d: 5, a2: undefined, b2: undefined, c2: 5, d2: 5, a3: { x: 5, y: 20 }, b3: { x: 5, y: 20 }, c3: { x: 5, y: 20 }, d3: { x: 5, y: 20 }, e3: { x: 5, y: 20 }, f3: { x: 5, y: 20 } } }) }) ================================================ FILE: __tests__/core/optional.test.ts ================================================ import { getSnapshot, types, unprotect, applySnapshot, cast } from "../../src" import { expect, test } from "bun:test" test("it should provide a default value, if no snapshot is provided", () => { const Row = types.model({ name: "", quantity: 0 }) const Factory = types.model({ rows: types.optional(types.array(Row), [{ name: "test" }]) }) const doc = Factory.create() expect(getSnapshot(doc)).toEqual({ rows: [{ name: "test", quantity: 0 }] }) }) test("it should use the snapshot if provided", () => { const Row = types.model({ name: "", quantity: 0 }) const Factory = types.model({ rows: types.optional(types.array(Row), [{ name: "test" }]) }) const doc = Factory.create({ rows: [{ name: "snapshot", quantity: 0 }] }) expect(getSnapshot(doc)).toEqual({ rows: [{ name: "snapshot", quantity: 0 }] }) }) if (process.env.NODE_ENV !== "production") { test("it should throw if default value is invalid snapshot", () => { const Row = types.model({ name: types.string, quantity: types.number }) const error = expect(() => { types.model({ rows: types.optional(types.array(Row), [{}] as any) }) }).toThrow() }) test("it should throw bouncing errors from its sub-type", () => { const Row = types.model({ name: types.string, quantity: types.number }) const RowList = types.optional(types.array(Row), []) const error = expect(() => { RowList.create([ { name: "a", quantity: 1 }, { name: "b", quantity: "x" } ] as any) }).toThrow() }) } test("it should accept a function to provide dynamic values", () => { let defaultValue = 1 const Factory = types.model({ a: types.optional(types.number, () => defaultValue) }) expect(getSnapshot(Factory.create())).toEqual({ a: 1 }) defaultValue = 2 expect(getSnapshot(Factory.create())).toEqual({ a: 2 }) defaultValue = "hello world!" as any if (process.env.NODE_ENV !== "production") { expect(() => Factory.create()).toThrow( `[mobx-state-tree] Error while converting \`"hello world!"\` to \`number\`:\n\n value \`"hello world!"\` is not assignable to type: \`number\` (Value is not a number).` ) } }) test("Values should reset to default if omitted in snapshot", () => { const Store = types.model({ todo: types.model({ id: types.identifier, done: false, title: "test", thing: types.frozen({}) }) }) const store = Store.create({ todo: { id: "2" } }) unprotect(store) store.todo.done = true expect(store.todo.done).toBe(true) store.todo = cast({ title: "stuff", id: "2" }) expect(store.todo.title).toBe("stuff") expect(store.todo.done).toBe(false) }) test("optional frozen should fallback to default value if snapshot is undefined", () => { const Store = types.model({ thing: types.frozen({}) }) const store = Store.create({ thing: null }) expect(store.thing).toBeNull() applySnapshot(store, {}) expect(store.thing).toBeDefined() expect(store.thing).toEqual({}) }) test("an instance is not a valid default value, snapshot or function that creates instance must be used", () => { const Row = types.model("Row", { name: "", quantity: 0 }) // passing a node directly, without a generator function expect(() => { types.model({ rows: types.optional(types.array(Row), types.array(Row).create()) }) }).toThrow( "default value cannot be an instance, pass a snapshot or a function that creates an instance/snapshot instead" ) // an alike node but created from a different yet equivalent type const e = expect(() => { const Factory = types.model({ rows: types.optional(types.array(Row), () => types.array(Row).create()) }) // we need to create the node for it to throw, since generator functions are typechecked when nodes are created // tslint:disable-next-line:no-unused-expression Factory.create() }) if (process.env.NODE_ENV === "production") { e.not.toThrow() } else { e.toThrow("Error while converting <> to `Row[]`") } { // a node created on a generator function of the exact same type const RowArray = types.array(Row) const Factory = types.model("Factory", { rows: types.optional(RowArray, () => RowArray.create()) }) const doc = Factory.create() expect(getSnapshot(doc)).toEqual({ rows: [] }) } }) test("undefined can work as a missing value", () => { const M = types.model({ x: types.union(types.undefined, types.number) }) const m1 = M.create({ x: 5 }) expect(m1.x).toBe(5) const m2 = M.create({ x: undefined }) expect(m2.x).toBeUndefined() const m3 = M.create({}) // is ok as well (even in TS) expect(m3.x).toBeUndefined() }) ================================================ FILE: __tests__/core/parent-properties.test.ts ================================================ import { types, getEnv, getParent, getPath, Instance } from "../../src" import { expect, test } from "bun:test" const ChildModel = types .model("Child", { parentPropertyIsNullAfterCreate: false, parentEnvIsNullAfterCreate: false, parentPropertyIsNullAfterAttach: false }) .views(self => { return { get parent(): IParentModelInstance { return getParent(self) } } }) .actions(self => ({ afterCreate() { self.parentPropertyIsNullAfterCreate = typeof self.parent.fetch === "undefined" self.parentEnvIsNullAfterCreate = typeof getEnv(self.parent).fetch === "undefined" }, afterAttach() { self.parentPropertyIsNullAfterAttach = typeof self.parent.fetch === "undefined" } })) const ParentModel = types .model("Parent", { child: types.optional(ChildModel, {}) }) .views(self => ({ get fetch() { return getEnv(self).fetch } })) interface IParentModelInstance extends Instance {} // NOTE: parents are now always created before children; // moreover, we do not actually have actions hash during object-node creation test("Parent property have value during child's afterCreate() event", () => { const mockFetcher = () => Promise.resolve(true) const parent = ParentModel.create({}, { fetch: mockFetcher }) // Because the child is created before the parent creation is finished, this one will yield `true` (the .fetch view is still undefined) expect(parent.child.parentPropertyIsNullAfterCreate).toBe(false) // ... but, the env is available expect(parent.child.parentEnvIsNullAfterCreate).toBe(false) }) test("Parent property has value during child's afterAttach() event", () => { const mockFetcher = () => Promise.resolve(true) const parent = ParentModel.create({}, { fetch: mockFetcher }) expect(parent.child.parentPropertyIsNullAfterAttach).toBe(false) }) test("#917", () => { const SubTodo = types .model("SubTodo", { id: types.optional(types.number, () => Math.random()), title: types.string, finished: false }) .views(self => ({ get path() { return getPath(self) } })) .actions(self => ({ toggle() { self.finished = !self.finished } })) const Todo = types .model("Todo", { id: types.optional(types.number, () => Math.random()), title: types.string, finished: false, subTodos: types.array(SubTodo) }) .views(self => ({ get path() { return getPath(self) } })) .actions(self => ({ toggle() { self.finished = !self.finished } })) const TodoStore = types .model("TodoStore", { todos: types.array(Todo) }) .views(self => ({ get unfinishedTodoCount() { return self.todos.filter(todo => !todo.finished).length } })) .actions(self => ({ addTodo(title: string) { self.todos.push({ title, subTodos: [ { title } ] }) } })) const store2 = TodoStore.create({ todos: [ Todo.create({ title: "get Coffee", subTodos: [ SubTodo.create({ title: "test" }) ] }) ] }) expect(store2.todos[0].path).toBe("/todos/0") expect(store2.todos[0].subTodos[0].path).toBe("/todos/0/subTodos/0") }) ================================================ FILE: __tests__/core/pointer.test.ts ================================================ import { types, unprotect, IAnyModelType, castToReferenceSnapshot } from "../../src" import { expect, test } from "bun:test" function Pointer(Model: IT) { return types.model("PointerOf" + Model.name, { value: types.maybe(types.reference(Model)) }) } const Todo = types.model("Todo", { id: types.identifier, name: types.string }) test("it should allow array of pointer objects", () => { const TodoPointer = Pointer(Todo) const AppStore = types.model("AppStore", { todos: types.array(Todo), selected: types.optional(types.array(TodoPointer), []) }) const store = AppStore.create({ todos: [ { id: "1", name: "Hello" }, { id: "2", name: "World" } ], selected: [] }) unprotect(store) const ref = TodoPointer.create({ value: castToReferenceSnapshot(store.todos[0]) }) // Fails because store.todos does not belongs to the same tree store.selected.push(ref) expect(store.selected[0].value).toBe(store.todos[0]) }) test("it should allow array of pointer objects - 2", () => { const TodoPointer = Pointer(Todo) const AppStore = types.model({ todos: types.array(Todo), selected: types.optional(types.array(TodoPointer), []) }) const store = AppStore.create({ todos: [ { id: "1", name: "Hello" }, { id: "2", name: "World" } ], selected: [] }) unprotect(store) const ref = TodoPointer.create() store.selected.push(ref) ref.value = store.todos[0] expect(store.selected[0].value).toBe(store.todos[0]) }) test("it should allow array of pointer objects - 3", () => { const TodoPointer = Pointer(Todo) const AppStore = types.model({ todos: types.array(Todo), selected: types.optional(types.array(TodoPointer), []) }) const store = AppStore.create({ todos: [ { id: "1", name: "Hello" }, { id: "2", name: "World" } ], selected: [] }) unprotect(store) const ref = TodoPointer.create({ value: castToReferenceSnapshot(store.todos[0]) }) store.selected.push(ref) expect(store.selected[0].value).toBe(store.todos[0]) }) test("it should allow array of pointer objects - 4", () => { const TodoPointer = Pointer(Todo) const AppStore = types.model({ todos: types.array(Todo), selected: types.optional(types.array(TodoPointer), []) }) const store = AppStore.create({ todos: [ { id: "1", name: "Hello" }, { id: "2", name: "World" } ], selected: [] }) unprotect(store) const ref = TodoPointer.create() // Fails because ref is required store.selected.push(ref) ref.value = store.todos[0] expect(ref.value).toBe(store.todos[0]) }) ================================================ FILE: __tests__/core/primitives.test.ts ================================================ import { isFinite, isFloat, isInteger } from "../../src/utils" import { types, applySnapshot, getSnapshot } from "../../src" import { expect, test } from "bun:test" test("Date instance can be reused", () => { const Model = types.model({ a: types.model({ b: types.string }), c: types.Date // types.string -> types.Date }) const Store = types .model({ one: Model, index: types.array(Model) }) .actions(self => { function set(one: typeof Model.Type) { self.one = one } function push(model: typeof Model.Type) { self.index.push(model) } return { set, push } }) const object = { a: { b: "string" }, c: new Date() } // string -> date (number) const instance = Store.create({ one: object, index: [object] }) instance.set(object) expect(() => instance.push(object)).not.toThrow() expect(instance.one.c).toBe(object.c) expect(instance.index[0].c).toBe(object.c) }) test("Date can be rehydrated using unix timestamp", () => { const time = new Date() const newTime = 6813823163 const Factory = types.model({ date: types.optional(types.Date, () => time) }) const store = Factory.create() expect(store.date.getTime()).toBe(time.getTime()) applySnapshot(store, { date: newTime }) expect(store.date.getTime()).toBe(newTime) expect(getSnapshot(store).date).toBe(newTime) }) test("check isInteger", () => { expect(isInteger(5)).toBe(true) expect(isInteger(-5)).toBe(true) expect(isInteger(5.2)).toBe(false) }) test("Default inference for integers is 'number'", () => { const A = types.model({ x: 3 }) expect( A.is({ x: 2.5 }) ).toBe(true) }) test("check isFloat", () => { expect(isFloat(3.14)).toBe(true) expect(isFloat(-2.5)).toBe(true) expect(isFloat(Infinity)).toBe(true) expect(isFloat(10)).toBe(false) expect(isFloat(0)).toBe(false) expect(isFloat("3.14")).toBe(false) expect(isFloat(null)).toBe(false) expect(isFloat(undefined)).toBe(false) expect(isFloat(NaN)).toBe(false) }) test("check isFinite", () => { expect(isFinite(3.14)).toBe(true) expect(isFinite(-2.5)).toBe(true) expect(isFinite(10)).toBe(true) expect(isFinite(0)).toBe(true) expect(isFinite("3.14")).toBe(false) expect(isFinite(null)).toBe(false) expect(isFinite(undefined)).toBe(false) expect(isFinite(NaN)).toBe(false) expect(isFinite(Infinity)).toBe(false) }) if (process.env.NODE_ENV !== "production") { test("Passing non integer to types.integer", () => { const Size = types.model({ width: types.integer, height: 20 }) expect(() => { const size = Size.create({ width: 10 }) }).not.toThrow() expect(() => { const size = Size.create({ width: 10.5 }) }).toThrow() }) } test("types.bigint accepts bigint values", () => { const Model = types.model({ id: types.bigint, value: types.bigint }) const instance = Model.create({ id: BigInt(1), value: BigInt(2) }) expect(instance.id).toBe(BigInt(1)) expect(instance.value).toBe(BigInt(2)) expect(types.bigint.describe()).toBe("bigint") }) test("types.bigint snapshot round-trip preserves bigint", () => { const Model = types.model({ id: types.bigint }) const instance = Model.create({ id: BigInt("9007199254740993") }) const snapshot = getSnapshot(instance) expect(snapshot.id).toBe("9007199254740993") expect(typeof snapshot.id).toBe("string") }) test("types.bigint snapshot is JSON-serializable and deserializes correctly", () => { const Model = types.model({ id: types.bigint, value: types.bigint }) const instance = Model.create({ id: BigInt(1), value: BigInt("9007199254740993") }) const snapshot = getSnapshot(instance) const json = JSON.stringify(snapshot) expect(json).toBe('{"id":"1","value":"9007199254740993"}') const parsed = JSON.parse(json) const restored = Model.create(parsed) expect(restored.id).toBe(BigInt(1)) expect(restored.value).toBe(BigInt("9007199254740993")) }) if (process.env.NODE_ENV !== "production") { test("Passing non bigint/string/number to types.bigint", () => { const Model = types.model({ id: types.bigint }) expect(() => Model.create({ id: BigInt(1) })).not.toThrow() expect(() => Model.create({ id: "1" })).not.toThrow() expect(() => Model.create({ id: 1 })).not.toThrow() expect(() => Model.create({ id: null as any })).toThrow() }) } ================================================ FILE: __tests__/core/protect.test.ts ================================================ import { protect, unprotect, applySnapshot, types, isProtected, getParent, cast } from "../../src" import { expect, test } from "bun:test" const Todo = types .model("Todo", { title: "" }) .actions(self => { function setTitle(newTitle: string) { self.title = newTitle } return { setTitle } }) const Store = types.model("Store", { todos: types.array(Todo) }) function createTestStore() { return Store.create({ todos: [{ title: "Get coffee" }, { title: "Get biscuit" }] }) } test("it should be possible to protect an object", () => { const store = createTestStore() unprotect(store) store.todos[1].title = "A" protect(store) expect(() => { store.todos[0].title = "B" }).toThrow( "[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action." ) expect(store.todos[1].title).toBe("A") expect(store.todos[0].title).toBe("Get coffee") store.todos[0].setTitle("B") expect(store.todos[0].title).toBe("B") }) test("protect should protect against any update", () => { const store = createTestStore() expect( // apply Snapshot / patch are currently allowed, even outside protected mode () => { applySnapshot(store, { todos: [{ title: "Get tea" }] }) } ).not.toThrow( "[mobx-state-tree] Cannot modify 'Todo@', the object is protected and can only be modified by using an action." ) expect(() => { store.todos.push({ title: "test" }) }).toThrow( "[mobx-state-tree] Cannot modify 'Todo[]@/todos', the object is protected and can only be modified by using an action." ) expect(() => { store.todos[0].title = "test" }).toThrow( "[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action." ) }) test("protect should also protect children", () => { const store = createTestStore() expect(() => { store.todos[0].title = "B" }).toThrow( "[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action." ) store.todos[0].setTitle("B") expect(store.todos[0].title).toBe("B") }) test("unprotected mode should be lost when attaching children", () => { const store = Store.create({ todos: [] }) const t1 = Todo.create({ title: "hello" }) unprotect(t1) expect(isProtected(t1)).toBe(false) expect(isProtected(store)).toBe(true) t1.title = "world" // ok unprotect(store) store.todos.push(t1) protect(store) expect(isProtected(t1)).toBe(true) expect(isProtected(store)).toBe(true) expect(() => { t1.title = "B" }).toThrow( "[mobx-state-tree] Cannot modify 'Todo@/todos/0', the object is protected and can only be modified by using an action." ) store.todos[0].setTitle("C") expect(store.todos[0].title).toBe("C") }) test("protected mode should be inherited when attaching children", () => { const store = Store.create({ todos: [] }) unprotect(store) const t1 = Todo.create({ title: "hello" }) expect(isProtected(t1)).toBe(true) expect(isProtected(store)).toBe(false) expect(() => { t1.title = "B" }).toThrow( "[mobx-state-tree] Cannot modify 'Todo@', the object is protected and can only be modified by using an action." ) store.todos.push(t1) t1.title = "world" // ok, now unprotected expect(isProtected(t1)).toBe(false) expect(isProtected(store)).toBe(false) expect(store.todos[0].title).toBe("world") }) test("action cannot modify parent", () => { const Child = types .model("Child", { x: 2 }) .actions(self => ({ setParentX() { getParent(self).x += 1 } })) const Parent = types.model("Parent", { x: 3, child: Child }) const p = Parent.create({ child: {} }) expect(() => p.child.setParentX()).toThrow( "[mobx-state-tree] Cannot modify 'Parent@', the object is protected and can only be modified by using an action." ) }) ================================================ FILE: __tests__/core/recordPatches.test.ts ================================================ import { getSnapshot, unprotect, recordPatches, types, IType, IJsonPatch, Instance, cast, IAnyModelType, IMSTMap } from "../../src" import { expect, test } from "bun:test" function testPatches( type: IType, snapshot: C, fn: any, expectedPatches: IJsonPatch[], expectedInversePatches: IJsonPatch[] ) { const instance = type.create(snapshot) const baseSnapshot = getSnapshot(instance) const recorder = recordPatches(instance) unprotect(instance) fn(instance) recorder.stop() expect(recorder.patches).toEqual(expectedPatches) expect(recorder.inversePatches).toEqual(expectedInversePatches) const clone = type.create(snapshot) recorder.replay(clone) expect(getSnapshot(clone)).toEqual(getSnapshot(instance)) recorder.undo() expect(getSnapshot(instance)).toEqual(baseSnapshot) } const Node = types.model("Node", { id: types.identifierNumber, text: "Hi", children: types.optional(types.array(types.late((): IAnyModelType => Node)), []) }) test("it should apply simple patch", () => { testPatches( Node, { id: 1 }, (n: Instance) => { n.text = "test" }, [ { op: "replace", path: "/text", value: "test" } ], [ { op: "replace", path: "/text", value: "Hi" } ] ) }) test("it should apply deep patches to arrays", () => { testPatches( Node, { id: 1, children: [{ id: 2 }] }, (n: Instance) => { const children = n.children as unknown as Instance[] children[0].text = "test" // update children[0] = cast({ id: 2, text: "world" }) // this reconciles; just an update children[0] = cast({ id: 4, text: "coffee" }) // new object children[1] = cast({ id: 3, text: "world" }) // addition children.splice(0, 1) // removal }, [ { op: "replace", path: "/children/0/text", value: "test" }, { op: "replace", path: "/children/0/text", value: "world" }, { op: "replace", path: "/children/0", value: { id: 4, text: "coffee", children: [] } }, { op: "add", path: "/children/1", value: { id: 3, text: "world", children: [] } }, { op: "remove", path: "/children/0" } ], [ { op: "replace", path: "/children/0/text", value: "Hi" }, { op: "replace", path: "/children/0/text", value: "test" }, { op: "replace", path: "/children/0", value: { children: [], id: 2, text: "world" } }, { op: "remove", path: "/children/1" }, { op: "add", path: "/children/0", value: { children: [], id: 4, text: "coffee" } } ] ) }) test("it should apply deep patches to maps", () => { const NodeMap = types.model("NodeMap", { id: types.identifierNumber, text: "Hi", children: types.optional(types.map(types.late((): IAnyModelType => NodeMap)), {}) }) testPatches( NodeMap, { id: 1, children: { 2: { id: 2 } } }, (n: Instance) => { const children = n.children as IMSTMap children.get("2")!.text = "test" // update children.put({ id: 2, text: "world" }) // this reconciles; just an update children.set( "4", NodeMap.create({ id: 4, text: "coffee", children: { 23: { id: 23 } } }) ) // new object children.put({ id: 3, text: "world", children: { 7: { id: 7 } } }) // addition children.delete("2") // removal }, [ { op: "replace", path: "/children/2/text", value: "test" }, { op: "replace", path: "/children/2/text", value: "world" }, { op: "add", path: "/children/4", value: { children: { 23: { children: {}, id: 23, text: "Hi" } }, id: 4, text: "coffee" } }, { op: "add", path: "/children/3", value: { children: { 7: { children: {}, id: 7, text: "Hi" } }, id: 3, text: "world" } }, { op: "remove", path: "/children/2" } ], [ { op: "replace", path: "/children/2/text", value: "Hi" }, { op: "replace", path: "/children/2/text", value: "test" }, { op: "remove", path: "/children/4" }, { op: "remove", path: "/children/3" }, { op: "add", path: "/children/2", value: { children: {}, id: 2, text: "world" } } ] ) }) test("it should apply deep patches to objects", () => { const NodeObject = types.model("NodeObject", { id: types.identifierNumber, text: "Hi", child: types.maybe(types.late((): IAnyModelType => NodeObject)) }) testPatches( NodeObject, { id: 1, child: { id: 2 } }, (n: Instance) => { n.child!.text = "test" // update n.child = cast({ id: 2, text: "world" }) // this reconciles; just an update n.child = NodeObject.create({ id: 2, text: "coffee", child: { id: 23 } }) n.child = cast({ id: 3, text: "world", child: { id: 7 } }) // addition n.child = undefined // removal }, [ { op: "replace", path: "/child/text", value: "test" }, { op: "replace", path: "/child/text", value: "world" }, { op: "replace", path: "/child", value: { child: { child: undefined, id: 23, text: "Hi" }, id: 2, text: "coffee" } }, { op: "replace", path: "/child", value: { child: { child: undefined, id: 7, text: "Hi" }, id: 3, text: "world" } }, { op: "replace", path: "/child", value: undefined } ], [ { op: "replace", path: "/child/text", value: "Hi" }, { op: "replace", path: "/child/text", value: "test" }, { op: "replace", path: "/child", value: { child: undefined, id: 2, text: "world" } }, { op: "replace", path: "/child", value: { child: { child: undefined, id: 23, text: "Hi" }, id: 2, text: "coffee" } }, { op: "replace", path: "/child", value: { child: { child: undefined, id: 7, text: "Hi" }, id: 3, text: "world" } } ] ) }) ================================================ FILE: __tests__/core/reference-custom.test.ts ================================================ import { reaction, when, values } from "mobx" import { types, recordPatches, getSnapshot, applySnapshot, applyPatch, unprotect, getRoot, onSnapshot, flow, Instance, resolveIdentifier } from "../../src" import { expect, test } from "bun:test" test("it should support custom references - basics", () => { const User = types.model({ id: types.identifier, name: types.string }) const UserByNameReference = types.maybeNull( types.reference(User, { // given an identifier, find the user get(identifier, parent): any { return ( (parent as Instance)!.users.find(u => u.name === identifier) || null ) }, // given a user, produce the identifier that should be stored set(value) { return value.name } }) ) const Store = types.model({ users: types.array(User), selection: UserByNameReference }) const s = Store.create({ users: [ { id: "1", name: "Michel" }, { id: "2", name: "Mattia" } ], selection: "Mattia" }) unprotect(s) expect(s.selection!.name).toBe("Mattia") expect(s.selection === s.users[1]).toBe(true) expect(getSnapshot(s).selection).toBe("Mattia") s.selection = s.users[0] expect(s.selection!.name).toBe("Michel") expect(s.selection === s.users[0]).toBe(true) expect(getSnapshot(s).selection).toBe("Michel") s.selection = null expect(getSnapshot(s).selection).toBe(null) applySnapshot(s, Object.assign({}, getSnapshot(s), { selection: "Mattia" })) // @ts-expect-error - typescript doesn't know that applySnapshot will update the selection expect(s.selection).toBe(s.users[1]) applySnapshot(s, Object.assign({}, getSnapshot(s), { selection: "Unknown" })) expect(s.selection).toBe(null) }) test("it should support custom references - adv", () => { const User = types.model({ id: types.identifier, name: types.string }) const NameReference = types.reference(User, { get(identifier, parent): any { if (identifier === null) return null const users = values(getRoot>(parent!).users) return users.filter(u => u.name === identifier)[0] || null }, set(value) { return value ? value.name : "" } }) const Store = types.model({ users: types.map(User), selection: NameReference }) const s = Store.create({ users: { "1": { id: "1", name: "Michel" }, "2": { id: "2", name: "Mattia" } }, selection: "Mattia" }) unprotect(s) expect(s.selection.name).toBe("Mattia") expect(s.selection === s.users.get("2")).toBe(true) expect(getSnapshot(s).selection).toBe("Mattia") const p = recordPatches(s) const r: any[] = [] onSnapshot(s, r.push.bind(r)) const ids: (string | null)[] = [] reaction( () => s.selection, selection => { ids.push(selection ? selection.id : null) } ) s.selection = s.users.get("1")! expect(s.selection.name).toBe("Michel") expect(s.selection === s.users.get("1")).toBe(true) expect(getSnapshot(s).selection).toBe("Michel") applySnapshot(s, Object.assign({}, getSnapshot(s), { selection: "Mattia" })) // @ts-expect-error - typescript doesn't know that applySnapshot will update the selection expect(s.selection).toBe(s.users.get("2")) applyPatch(s, { op: "replace", path: "/selection", value: "Michel" }) // @ts-expect-error - typescript doesn't know that applyPatch will update the selection expect(s.selection).toBe(s.users.get("1")) s.users.delete("1") // @ts-expect-error - typescript doesn't know how delete will affect the selection expect(s.selection).toBe(null) s.users.put({ id: "3", name: "Michel" }) expect(s.selection.id).toBe("3") expect(ids).toMatchSnapshot() expect(r).toMatchSnapshot() expect(p.patches).toMatchSnapshot() expect(p.inversePatches).toMatchSnapshot() }) test("it should support dynamic loading", async () => { const events: string[] = [] const User = types.model({ name: types.string, age: 0 }) const UserByNameReference = types.maybe( types.reference(User, { get(identifier: string, parent): any { return (parent as Instance).getOrLoadUser(identifier) }, set(value) { return value.name } }) ) const Store = types .model({ users: types.array(User), selection: UserByNameReference }) .actions(self => ({ loadUser: flow(function* loadUser(name: string) { events.push("loading " + name) self.users.push({ name }) yield new Promise(resolve => { setTimeout(resolve, 200) }) events.push("loaded " + name) const user = (self.users.find(u => u.name === name)!.age = name.length * 3) // wonderful! }) })) .views(self => ({ // Important: a view so that the reference will automatically react to the reference being changed! getOrLoadUser(name: string) { const user = self.users.find(u => u.name === name) || null if (!user) { /* TODO: this is ugly, but workaround the idea that views should be side effect free. We need a more elegant solution.. */ setImmediate(() => self.loadUser(name)) } return user } })) const s = Store.create({ users: [], selection: "Mattia" }) unprotect(s) expect(events).toEqual([]) expect(s.users.length).toBe(0) // @ts-expect-error - typescript doesn't know that the user will be loaded expect(s.selection).toBe(null) await when(() => s.users.length === 1 && s.users[0].age === 18 && s.users[0].name === "Mattia") expect(s.selection).toBe(s.users[0]) expect(events).toEqual(["loading Mattia", "loaded Mattia"]) }) test("custom reference / safe custom reference to another store works", () => { const Todo = types.model({ id: types.identifier }) const TodoStore = types.model({ todos: types.array(Todo) }) const OtherStore = types.model({ todoRef: types.maybe( types.reference(Todo, { get(id) { const node = resolveIdentifier(Todo, todos, id) if (!node) { throw new Error("Invalid ref") } return node }, set(value) { return value.id } }) ), safeRef: types.safeReference(Todo, { get(id) { const node = resolveIdentifier(Todo, todos, id) if (!node) { throw new Error("Invalid ref") } return node }, set(value) { return value.id } }) }) const todos = TodoStore.create({ todos: [{ id: "1" }, { id: "2" }, { id: "3" }] }) unprotect(todos) // from a snapshot const otherStore = OtherStore.create({ todoRef: "1", safeRef: "1" }) unprotect(otherStore) expect(otherStore.todoRef!.id).toBe("1") expect(otherStore.safeRef!.id).toBe("1") // assigning an id otherStore.todoRef = "2" as any otherStore.safeRef = "2" as any expect(otherStore.todoRef!.id).toBe("2") expect(otherStore.safeRef!.id).toBe("2") // assigning a node directly otherStore.todoRef = todos.todos[2] otherStore.safeRef = todos.todos[2] expect(otherStore.todoRef!.id).toBe("3") expect(otherStore.safeRef!.id).toBe("3") // getting the snapshot expect(getSnapshot(otherStore)).toEqual({ todoRef: "3", safeRef: "3" }) // the removed node should throw on standard refs access // and be set to undefined on safe ones todos.todos.splice(2, 1) expect(() => otherStore.todoRef).toThrow("Invalid ref") expect(otherStore.safeRef).toBeUndefined() }) ================================================ FILE: __tests__/core/reference-onInvalidated.test.ts ================================================ import { types, OnReferenceInvalidated, Instance, ReferenceIdentifier, IAnyStateTreeNode, unprotect, OnReferenceInvalidatedEvent, getSnapshot, applySnapshot, clone, destroy } from "../../src" import { describe, expect, it, test } from "bun:test" const Todo = types.model({ id: types.identifier }) const createSnapshot = (partialSnapshot: any) => ({ todos: [{ id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }], ...partialSnapshot }) const createStore = ( partialSnapshot: any, onInvalidated?: OnReferenceInvalidated>, customRef = false ) => { const refOptions = { onInvalidated, get(identifier: ReferenceIdentifier, parent: IAnyStateTreeNode | null) { return (parent as Instance).todos.find(t => t.id === identifier) }, set(value: Instance): ReferenceIdentifier { return value.id } } if (!customRef) { // @ts-ignore delete refOptions.get // @ts-ignore delete refOptions.set } const Store = types.model({ todos: types.array(Todo), onInv: types.maybe(types.reference(Todo, refOptions as any)), single: types.safeReference(Todo), deep: types.optional( types.model({ single: types.safeReference(Todo) }), {} ), arr: types.array(types.safeReference(Todo)), map: types.map(types.safeReference(Todo)) }) const s = Store.create(createSnapshot(partialSnapshot)) unprotect(s) return s } for (const customRef of [false, true]) { describe(`onInvalidated - customRef: ${customRef}`, () => { test("from snapshot without accessing the referenced node", () => { let ev: OnReferenceInvalidatedEvent> | undefined let oldRefId!: string let calls = 0 const onInv: OnReferenceInvalidated> = ev1 => { calls++ oldRefId = ev1.invalidTarget!.id expect(ev1.invalidId).toBe(oldRefId) ev = ev1 ev1.removeRef() } const store = createStore({ onInv: "1" }, onInv) expect(calls).toBe(0) store.todos.splice(0, 1) expect(calls).toBe(1) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("1") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv).toBeUndefined() expect(getSnapshot(store).onInv).toBeUndefined() store.onInv = store.todos[0] expect(calls).toBe(1) store.todos.splice(0, 1) expect(calls).toBe(2) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("2") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv).toBeUndefined() expect(getSnapshot(store).onInv).toBeUndefined() }) test("applying snapshot without accesing the referenced node", () => { let ev: OnReferenceInvalidatedEvent> | undefined let oldRefId!: string let calls = 0 const onInv: OnReferenceInvalidated> = ev1 => { calls++ oldRefId = ev1.invalidTarget!.id expect(ev1.invalidId).toBe(oldRefId) ev = ev1 ev1.removeRef() } const store = createStore({}, onInv) expect(calls).toBe(0) applySnapshot(store, createSnapshot({ onInv: "1" })) expect(calls).toBe(0) store.todos.splice(0, 1) expect(calls).toBe(1) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("1") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv).toBeUndefined() expect(getSnapshot(store).onInv).toBeUndefined() store.onInv = store.todos[0] expect(calls).toBe(1) store.todos.splice(0, 1) expect(calls).toBe(2) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("2") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv).toBeUndefined() expect(getSnapshot(store).onInv).toBeUndefined() }) test("runtime change", () => { let ev: OnReferenceInvalidatedEvent> | undefined let oldRefId!: string let calls = 0 const onInv: OnReferenceInvalidated> = ev1 => { calls++ oldRefId = ev1.invalidTarget!.id expect(ev1.invalidId).toBe(oldRefId) ev = ev1 ev1.removeRef() } const store = createStore({}, onInv) expect(calls).toBe(0) store.onInv = store.todos[1] expect(calls).toBe(0) store.onInv = store.todos[0] expect(calls).toBe(0) store.todos.remove(store.todos[0]) expect(calls).toBe(1) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("1") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv).toBeUndefined() expect(getSnapshot(store).onInv).toBeUndefined() store.onInv = store.todos[0] expect(calls).toBe(1) store.todos.remove(store.todos[0]) expect(calls).toBe(2) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("2") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv).toBeUndefined() expect(getSnapshot(store).onInv).toBeUndefined() }) test("replacing ref", () => { let ev: OnReferenceInvalidatedEvent> | undefined let oldRefId!: string let calls = 0 const onInv: OnReferenceInvalidated> = ev1 => { calls++ oldRefId = ev1.invalidTarget!.id expect(ev1.invalidId).toBe(oldRefId) ev = ev1 ev1.replaceRef(store.todos[1]) } const store = createStore({}, onInv) expect(calls).toBe(0) store.onInv = store.todos[0] expect(calls).toBe(0) store.todos.remove(store.todos[0]) expect(calls).toBe(1) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("1") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv!.id).toBe("2") expect(getSnapshot(store).onInv).toBe("2") }) test("cloning works", () => { let ev: OnReferenceInvalidatedEvent> | undefined let oldRefId!: string let calls = 0 const onInv: OnReferenceInvalidated> = ev1 => { calls++ oldRefId = ev1.invalidTarget!.id expect(ev1.invalidId).toBe(oldRefId) ev = ev1 ev1.removeRef() } const store1 = createStore({}, onInv) expect(calls).toBe(0) store1.onInv = store1.todos[0] expect(calls).toBe(0) const store = clone(store1) unprotect(store) expect(calls).toBe(0) store.onInv = store.todos[0] expect(calls).toBe(0) store.todos.remove(store.todos[0]) expect(calls).toBe(1) expect(ev!.parent).toBe(store) expect(oldRefId).toBe("1") expect(ev!.removeRef).toBeTruthy() expect(ev!.replaceRef).toBeTruthy() expect(store.onInv).toBeUndefined() expect(getSnapshot(store).onInv).toBeUndefined() // make sure other ref stil points to the right one expect(store1.onInv).toBe(store1.todos[0]) }) }) } describe("safeReference", () => { test("model property", () => { const store = createStore({}) expect(store.single).toBeUndefined() store.single = store.todos[0] expect(store.single).toBe(store.todos[0]) store.todos.remove(store.todos[0]) expect(store.single).toBeUndefined() }) test("deep model property", () => { const store = createStore({}) expect(store.deep.single).toBeUndefined() store.deep.single = store.todos[0] expect(store.deep.single).toBe(store.todos[0]) store.todos.remove(store.todos[0]) expect(store.deep.single).toBeUndefined() }) test("array child", () => { const store = createStore({}) expect(store.arr.length).toBe(0) store.arr.push(store.todos[0]) store.arr.push(store.todos[2]) expect(store.arr.length).toBe(2) expect(store.arr[0]!.id).toBe("1") expect(store.arr[1]!.id).toBe("3") store.todos.splice(0, 1) expect(store.arr.length).toBe(1) expect(store.arr[0]!.id).toBe("3") }) test("map child", () => { const store = createStore({}) expect(store.map.size).toBe(0) store.map.set("a", store.todos[0]) store.map.set("c", store.todos[2]) expect(store.map.size).toBe(2) expect(store.map.get("a")!.id).toBe("1") expect(store.map.get("c")!.id).toBe("3") store.todos.splice(0, 1) expect(store.map.size).toBe(1) expect(store.map.get("c")!.id).toBe("3") }) test("invalid references in a snapshot should be removed", () => { const store = createStore({ single: "100", arr: ["100", "1"], map: { a: "100", b: "1" } }) expect(store.single).toBeUndefined() expect(store.arr.length).toBe(1) expect(store.arr[0]!.id).toBe("1") expect(store.map.size).toBe(1) expect(store.map.get("b")!.id).toBe("1") // check reassignation still works store.single = store.todos[0] expect(store.single).toBe(store.todos[0]) store.todos.remove(store.todos[0]) expect(store.single).toBeUndefined() }) test("setting it to an invalid id and then accessing it should still result in an error", () => { const store = createStore({}) store.single = "100" as any expect(() => { const s = store.single }).toThrow("Failed to resolve reference") }) }) test("#1115 - safe reference doesn't become invalidated when the reference has never been acessed", () => { const MyRefModel = types.model("MyRefModel", { id: types.identifier }) const SafeRef = types.model("SafeRef", { ref: types.safeReference(MyRefModel) }) const RootModel = types .model("RootModel", { mapOfRef: types.map(MyRefModel), arrayOfSafeRef: types.array(SafeRef) }) .actions(self => ({ deleteSqr(id: string) { self.mapOfRef.delete(id) } })) const rootModel = RootModel.create({ mapOfRef: { sqr1: { id: "sqr1" }, sqr2: { id: "sqr2" } }, arrayOfSafeRef: [ { ref: "sqr2" }, { ref: "sqr1" }, { ref: "sqr2" } ] }) expect(getSnapshot(rootModel.arrayOfSafeRef)).toEqual([ { ref: "sqr2" }, { ref: "sqr1" }, { ref: "sqr2" } ]) rootModel.deleteSqr("sqr1") expect(getSnapshot(rootModel.arrayOfSafeRef)).toEqual([ { ref: "sqr2" }, { ref: undefined }, { ref: "sqr2" } ]) rootModel.deleteSqr("sqr2") expect(getSnapshot(rootModel.arrayOfSafeRef)).toEqual([ { ref: undefined }, { ref: undefined }, { ref: undefined } ]) }) describe("safeReference with acceptsUndefined: false", () => { const MyRefModel = types.model("MyRefModel", { id: types.identifier }) const SafeRef = types.safeReference(MyRefModel, { acceptsUndefined: false }) it("removes invalidates items from map/array", () => { const Store = types.model({ todos: types.array(MyRefModel), arr: types.array(SafeRef), map: types.map(SafeRef) }) const store = Store.create({ todos: [{ id: "1" }, { id: "2" }], arr: ["1", "2"], map: { a1: "1", a2: "2" } }) unprotect(store) // just to check TS is happy with this const arr: Instance[] = store.arr store.todos.splice(0, 1) expect(store.arr.length).toBe(1) expect(store.map.size).toBe(1) }) if (process.env.NODE_ENV !== "production") { it("throws when a model property is invalidated", () => { const Store = types.model({ todos: types.array(MyRefModel), single: SafeRef }) const store = Store.create({ todos: [{ id: "1" }, { id: "2" }], single: "1" }) unprotect(store) expect(() => { store.todos.splice(0, 1) }).toThrow("value `undefined` is not assignable to type") }) it("does not accept undefined in the array", () => { const Store = types.model({ todos: types.array(MyRefModel), arr: types.array(SafeRef) }) expect(() => Store.create({ todos: [{ id: "1" }, { id: "2" }], arr: ["1", undefined as any] }) ).toThrow("value `undefined` is not assignable to type") }) it("does not accept undefined in the map", () => { const Store = types.model({ todos: types.array(MyRefModel), map: types.map(SafeRef) }) expect(() => Store.create({ todos: [{ id: "1" }, { id: "2" }], map: { a1: "1", a2: undefined as any } }) ).toThrow("value `undefined` is not assignable to type") }) } }) test("#1275 - removing an object from a map should result in the snapshot of references being modified", () => { const Item = types.model({ id: types.identifier }) const Root = types.model({ items: types.map(Item), refs: types.array(types.safeReference(Item)) }) const thing = Root.create({ items: { aa: { id: "a" }, bb: { id: "b" }, cc: { id: "c" } }, refs: ["a", "b", "c"] }) unprotect(thing) destroy(thing.items.get("bb")!) expect(getSnapshot(thing.refs)).toEqual(["a", "c"]) }) ================================================ FILE: __tests__/core/reference.test.ts ================================================ import { reaction, autorun, isObservable, configure } from "mobx" import { types, getSnapshot, applySnapshot, onPatch, applyPatch, unprotect, detach, resolveIdentifier, getRoot, cast, SnapshotOut, IAnyModelType, Instance, SnapshotOrInstance, isAlive, destroy, castToReferenceSnapshot, tryReference, isValidReference, isStateTreeNode, addDisposer } from "../../src" import { expect, jest, test } from "bun:test" test("it should support prefixed paths in maps", () => { const User = types.model({ id: types.identifier, name: types.string }) const UserStore = types.model({ user: types.reference(User), users: types.map(User) }) const store = UserStore.create({ user: "17", users: { "17": { id: "17", name: "Michel" }, "18": { id: "18", name: "Veria" } } }) unprotect(store) expect(store.users.get("17")!.name).toBe("Michel") expect(store.users.get("18")!.name).toBe("Veria") expect(store.user.name).toBe("Michel") store.user = store.users.get("18")! expect(store.user.name).toBe("Veria") store.users.get("18")!.name = "Noa" expect(store.user.name).toBe("Noa") expect(getSnapshot(store)).toEqual({ user: "18", users: { "17": { id: "17", name: "Michel" }, "18": { id: "18", name: "Noa" } } } as SnapshotOut) }) test("it should support prefixed paths in arrays", () => { const User = types.model({ id: types.identifier, name: types.string }) const UserStore = types.model({ user: types.reference(User), users: types.array(User) }) const store = UserStore.create({ user: "17", users: [ { id: "17", name: "Michel" }, { id: "18", name: "Veria" } ] }) unprotect(store) expect(store.users[0].name).toBe("Michel") expect(store.users[1].name).toBe("Veria") expect(store.user.name).toBe("Michel") store.user = store.users[1] expect(store.user.name).toBe("Veria") store.users[1].name = "Noa" expect(store.user.name).toBe("Noa") expect(getSnapshot(store)).toEqual({ user: "18", users: [ { id: "17", name: "Michel" }, { id: "18", name: "Noa" } ] } as SnapshotOut) }) if (process.env.NODE_ENV !== "production") { test("identifiers are required", () => { const Todo = types.model({ id: types.identifier }) expect(Todo.is({})).toBe(false) expect(Todo.is({ id: "x" })).toBe(true) expect(() => (Todo.create as any)()).toThrow( " `undefined` is not assignable to type: `identifier` (Value is not a valid identifier, expected a string)" ) }) test("identifiers cannot be modified", () => { const Todo = types.model({ id: types.identifier }) const todo = Todo.create({ id: "x" }) unprotect(todo) expect(() => (todo.id = "stuff")).toThrow( "[mobx-state-tree] Tried to change identifier from 'x' to 'stuff'. Changing identifiers is not allowed." ) expect(() => applySnapshot(todo, { id: "stuff" })).toThrow( "[mobx-state-tree] Tried to change identifier from 'x' to 'stuff'. Changing identifiers is not allowed." ) }) } test("it should resolve refs during creation, when using path", () => { const values: number[] = [] const Book = types.model({ id: types.identifier, price: types.number }) const BookEntry = types .model({ book: types.reference(Book) }) .views(self => ({ get price() { return self.book.price * 2 } })) const Store = types.model({ books: types.array(Book), entries: types.optional(types.array(BookEntry), []) }) const s = Store.create({ books: [{ id: "3", price: 2 }] }) unprotect(s) reaction( () => s.entries.reduce((a, e) => a + e.price, 0), v => values.push(v) ) s.entries.push({ book: castToReferenceSnapshot(s.books[0]) }) expect(s.entries[0].price).toBe(4) expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(4) const entry = BookEntry.create({ book: castToReferenceSnapshot(s.books[0]) }) // N.B. ref is initially not resolvable! s.entries.push(entry) expect(s.entries[1].price).toBe(4) expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(8) expect(values).toEqual([4, 8]) }) test("it should resolve refs over late types", () => { const Book = types.model({ id: types.identifier, price: types.number }) const BookEntry = types .model({ book: types.reference(types.late(() => Book)) }) .views(self => ({ get price() { return self.book.price * 2 } })) const Store = types.model({ books: types.array(Book), entries: types.array(BookEntry) }) const s = Store.create({ books: [{ id: "3", price: 2 }] }) unprotect(s) s.entries.push({ book: castToReferenceSnapshot(s.books[0]) }) expect(s.entries[0].price).toBe(4) expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(4) }) test("it should resolve refs during creation, when using generic reference", () => { const values: number[] = [] const Book = types.model({ id: types.identifier, price: types.number }) const BookEntry = types .model({ book: types.reference(Book) }) .views(self => ({ get price() { return self.book.price * 2 } })) const Store = types.model({ books: types.array(Book), entries: types.optional(types.array(BookEntry), []) }) const s = Store.create({ books: [{ id: "3", price: 2 }] }) unprotect(s) reaction( () => s.entries.reduce((a, e) => a + e.price, 0), v => values.push(v) ) s.entries.push({ book: castToReferenceSnapshot(s.books[0]) }) expect(s.entries[0].price).toBe(4) expect(s.entries.reduce((a, e) => a + e.price, 0)).toBe(4) const entry = BookEntry.create({ book: castToReferenceSnapshot(s.books[0]) }) // can refer to book, even when not part of tree yet expect(getSnapshot(entry)).toEqual({ book: "3" }) s.entries.push(entry) expect(values).toEqual([4, 8]) }) test("identifiers should support subtypes of types.string and types.number", () => { const M = types.model({ id: types.refinement(types.identifierNumber, n => n > 5) }) expect(M.is({})).toBe(false) expect(M.is({ id: "test" })).toBe(false) expect(M.is({ id: "6" })).toBe(false) expect(M.is({ id: "4" })).toBe(false) expect(M.is({ id: 6 })).toBe(true) expect(M.is({ id: 4 })).toBe(false) const S = types.model({ mies: types.map(M), ref: types.reference(M) }) const s = S.create({ mies: { "7": { id: 7 } }, ref: "7" }) expect(s.mies.get("7")).toBeTruthy() expect(s.ref).toBe(s.mies.get("7")!) }) test("string identifiers should not accept numbers", () => { const F = types.model({ id: types.identifier }) expect(F.is({ id: "4" })).toBe(true) expect(F.is({ id: 4 })).toBe(false) const F2 = types.model({ id: types.identifier }) expect(F2.is({ id: "4" })).toBe(true) expect(F2.is({ id: 4 })).toBe(false) }) test("122 - identifiers should support numbers as well", () => { const F = types.model({ id: types.identifierNumber }) expect( F.create({ id: 3 }).id ).toBe(3) expect(F.is({ id: 4 })).toBe(true) expect(F.is({ id: "4" })).toBe(false) expect(F.is({ id: "bla" })).toBe(false) }) test("self reference with a late type", () => { const Book = types.model("Book", { id: types.identifier, genre: types.string, reference: types.reference(types.late((): IAnyModelType => Book)) }) const Store = types .model("Store", { books: types.array(Book) }) .actions(self => { function addBook(book: SnapshotOrInstance) { self.books.push(book) } return { addBook } }) const s = Store.create({ books: [{ id: "1", genre: "thriller", reference: "" }] }) const book2 = Book.create({ id: "2", genre: "romance", reference: castToReferenceSnapshot(s.books[0]) }) s.addBook(book2) expect((s.books[1].reference as Instance).genre).toBe("thriller") }) test("when applying a snapshot, reference should resolve correctly if value added after", () => { const Box = types.model({ id: types.identifierNumber, name: types.string }) const Factory = types.model({ selected: types.reference(Box), boxes: types.array(Box) }) expect(() => Factory.create({ selected: 1, boxes: [ { id: 1, name: "hello" }, { id: 2, name: "world" } ] }) ).not.toThrow() }) test("it should fail when reference snapshot is ambiguous", () => { const Box = types.model("Box", { id: types.identifierNumber, name: types.string }) const Arrow = types.model("Arrow", { id: types.identifierNumber, name: types.string }) const BoxOrArrow = types.union(Box, Arrow) const Factory = types.model({ selected: types.reference(BoxOrArrow), boxes: types.array(Box), arrows: types.array(Arrow) }) const store = Factory.create({ selected: 2, boxes: [ { id: 1, name: "hello" }, { id: 2, name: "world" } ], arrows: [{ id: 2, name: "arrow" }] }) expect(() => { // tslint:disable-next-line:no-unused-expression store.selected // store.boxes[1] // throws because it can't know if you mean a box or an arrow! }).toThrow( "[mobx-state-tree] Cannot resolve a reference to type '(Box | Arrow)' with id: '2' unambigously, there are multiple candidates: /boxes/1, /arrows/0" ) unprotect(store) // first update the reference, than create a new matching item! Ref becomes ambigous now... store.selected = 1 as any // valid assignment expect(store.selected).toBe(store.boxes[0]) // unambigous identifier let err!: Error autorun(() => store.selected, { onError(e) { err = e } }) expect(store.selected).toBe(store.boxes[0]) // unambigous identifier store.arrows.push({ id: 1, name: "oops" }) expect(err.message).toBe( "[mobx-state-tree] Cannot resolve a reference to type '(Box | Arrow)' with id: '1' unambigously, there are multiple candidates: /boxes/0, /arrows/1" ) }) test("it should support array of references", () => { const Box = types.model({ id: types.identifierNumber, name: types.string }) const Factory = types.model({ selected: types.array(types.reference(Box)), boxes: types.array(Box) }) const store = Factory.create({ selected: [], boxes: [ { id: 1, name: "hello" }, { id: 2, name: "world" } ] }) unprotect(store) expect(() => { store.selected.push(store.boxes[0]) }).not.toThrow() expect(getSnapshot(store.selected)).toEqual([1]) expect(() => { store.selected.push(store.boxes[1]) }).not.toThrow() expect(getSnapshot(store.selected)).toEqual([1, 2]) }) test("it should restore array of references from snapshot", () => { const Box = types.model({ id: types.identifierNumber, name: types.string }) const Factory = types.model({ selected: types.array(types.reference(Box)), boxes: types.array(Box) }) const store = Factory.create({ selected: [1, 2], boxes: [ { id: 1, name: "hello" }, { id: 2, name: "world" } ] }) unprotect(store) expect(store.selected[0] === store.boxes[0]).toEqual(true) expect(store.selected[1] === store.boxes[1]).toEqual(true) }) test("it should support map of references", () => { const Box = types.model({ id: types.identifierNumber, name: types.string }) const Factory = types.model({ selected: types.map(types.reference(Box)), boxes: types.array(Box) }) const store = Factory.create({ selected: {}, boxes: [ { id: 1, name: "hello" }, { id: 2, name: "world" } ] }) unprotect(store) expect(() => { store.selected.set("from", store.boxes[0]) }).not.toThrow() expect(getSnapshot(store.selected)).toEqual({ from: 1 }) expect(() => { store.selected.set("to", store.boxes[1]) }).not.toThrow() expect(getSnapshot(store.selected)).toEqual({ from: 1, to: 2 }) }) test("it should restore map of references from snapshot", () => { const Box = types.model({ id: types.identifierNumber, name: types.string }) const Factory = types.model({ selected: types.map(types.reference(Box)), boxes: types.array(Box) }) const store = Factory.create({ selected: { from: 1, to: 2 }, boxes: [ { id: 1, name: "hello" }, { id: 2, name: "world" } ] }) unprotect(store) expect(store.selected.get("from") === store.boxes[0]).toEqual(true) expect(store.selected.get("to") === store.boxes[1]).toEqual(true) }) test("it should support relative lookups", () => { const Node = types.model({ id: types.identifierNumber, children: types.optional(types.array(types.late((): IAnyModelType => Node)), []) }) const root = Node.create({ id: 1, children: [ { id: 2, children: [ { id: 4 } ] }, { id: 3 } ] }) unprotect(root) expect(getSnapshot(root)).toEqual({ id: 1, children: [ { id: 2, children: [{ id: 4, children: [] }] }, { id: 3, children: [] } ] }) expect(resolveIdentifier(Node, root, 1)).toBe(root) expect(resolveIdentifier(Node, root, 4)).toBe(root.children[0].children[0]) expect(resolveIdentifier(Node, root.children[0].children[0], 3)).toBe(root.children[1]) const n2 = detach(root.children[0]) unprotect(n2) expect(resolveIdentifier(Node, n2, 2)).toBe(n2) expect(resolveIdentifier(Node, root, 2)).toBeUndefined() expect(resolveIdentifier(Node, root, 4)).toBeUndefined() expect(resolveIdentifier(Node, n2, 3)).toBeUndefined() expect(resolveIdentifier(Node, n2, 4)).toBe(n2.children[0]) expect(resolveIdentifier(Node, n2.children[0], 2)).toBe(n2) const n5 = Node.create({ id: 5 }) expect(resolveIdentifier(Node, n5, 4)).toBeUndefined() n2.children.push(n5) expect(resolveIdentifier(Node, n5, 4)).toBe(n2.children[0]) expect(resolveIdentifier(Node, n2.children[0], 5)).toBe(n5) }) test("References are non-nullable by default", () => { const Todo = types.model({ id: types.identifierNumber }) const Store = types.model({ todo: types.maybe(Todo), ref: types.reference(Todo), maybeRef: types.maybe(types.reference(Todo)) }) expect(Store.is({})).toBe(false) expect(Store.is({ ref: 3 })).toBe(true) expect(Store.is({ ref: null })).toBe(false) expect(Store.is({ ref: undefined })).toBe(false) expect(Store.is({ ref: 3, maybeRef: 3 })).toBe(true) expect(Store.is({ ref: 3, maybeRef: undefined })).toBe(true) let store = Store.create({ todo: { id: 3 }, ref: 3 }) expect(store.ref).toBe(store.todo!) expect(store.maybeRef).toBeUndefined() store = Store.create({ todo: { id: 3 }, ref: 4 }) unprotect(store) if (process.env.NODE_ENV !== "production") { expect(store.maybeRef).toBeUndefined() expect(() => store.ref).toThrow( "[mobx-state-tree] Failed to resolve reference '4' to type 'AnonymousModel' (from node: /ref)" ) store.maybeRef = 3 as any // valid assignment expect(store.maybeRef).toBe(store.todo!) store.maybeRef = 4 as any // valid assignment expect(() => store.maybeRef).toThrow( "[mobx-state-tree] Failed to resolve reference '4' to type 'AnonymousModel' (from node: /maybeRef)" ) store.maybeRef = undefined expect(store.maybeRef).toBe(undefined) expect(() => ((store as any).ref = undefined)).toThrow(/Error while converting/) } }) test("References are described properly", () => { const Todo = types.model({ id: types.identifierNumber }) const Store = types.model({ todo: types.maybe(Todo), ref: types.reference(Todo), maybeRef: types.maybe(types.reference(Todo)) }) expect(Store.describe()).toBe( "{ todo: ({ id: identifierNumber } | undefined?); ref: reference(AnonymousModel); maybeRef: (reference(AnonymousModel) | undefined?) }" ) }) test("References in recursive structures", () => { const Folder = types.model("Folder", { id: types.identifierNumber, name: types.string, files: types.array(types.string) }) const Tree = types .model("Tree", { // sadly, this becomes any, and further untypeable... children: types.array(types.late((): IAnyModelType => Tree)), data: types.maybeNull(types.reference(Folder)) }) .actions(self => { function addFolder(data: SnapshotOrInstance) { const folder3 = Folder.create(data) getRoot(self).putFolderHelper(folder3) self.children.push( Tree.create({ data: castToReferenceSnapshot(folder3), children: [] }) ) } return { addFolder } }) const Storage = types .model("Storage", { objects: types.map(Folder), tree: Tree }) .actions(self => ({ putFolderHelper(aFolder: SnapshotOrInstance) { self.objects.put(aFolder) } })) const store = Storage.create({ objects: {}, tree: { children: [], data: null } }) const folder = { id: 1, name: "Folder 1", files: ["a.jpg", "b.jpg"] } store.tree.addFolder(folder) expect(getSnapshot(store)).toEqual({ objects: { "1": { files: ["a.jpg", "b.jpg"], id: 1, name: "Folder 1" } }, tree: { children: [ { children: [], data: 1 } ], data: null } }) expect(store.objects.get("1")).toBe(store.tree.children[0].data) const folder2 = { id: 2, name: "Folder 2", files: ["c.jpg", "d.jpg"] } store.tree.children[0].addFolder(folder2) expect(getSnapshot(store)).toEqual({ objects: { "1": { files: ["a.jpg", "b.jpg"], id: 1, name: "Folder 1" }, "2": { files: ["c.jpg", "d.jpg"], id: 2, name: "Folder 2" } }, tree: { children: [ { children: [ { children: [], data: 2 } ], data: 1 } ], data: null } }) expect(store.objects.get("1")).toBe(store.tree.children[0].data) expect(store.objects.get("2")).toBe(store.tree.children[0].children[0].data) }) test("it should applyPatch references in array", () => { const Item = types.model("Item", { id: types.identifier, name: types.string }) const Folder = types .model("Folder", { id: types.identifier, objects: types.map(Item), hovers: types.array(types.reference(Item)) }) .actions(self => { function addObject(anItem: typeof Item.Type) { self.objects.put(anItem) } function addHover(anItem: typeof Item.Type) { self.hovers.push(anItem) } function removeHover(anItem: typeof Item.Type) { self.hovers.remove(anItem) } return { addObject, addHover, removeHover } }) const folder = Folder.create({ id: "folder 1", objects: {}, hovers: [] }) folder.addObject({ id: "item 1", name: "item name 1" }) const item = folder.objects.get("item 1")! const snapshot = getSnapshot(folder) const newStore = Folder.create(snapshot) onPatch(folder, data => { applyPatch(newStore, data) }) folder.addHover(item) expect(getSnapshot(newStore)).toEqual({ id: "folder 1", objects: { "item 1": { id: "item 1", name: "item name 1" } }, hovers: ["item 1"] }) folder.removeHover(item) expect(getSnapshot(newStore)).toEqual({ id: "folder 1", objects: { "item 1": { id: "item 1", name: "item name 1" } }, hovers: [] }) }) test("it should applySnapshot references in array", () => { const Item = types.model("Item", { id: types.identifier, name: types.string }) const Folder = types.model("Folder", { id: types.identifier, objects: types.map(Item), hovers: types.array(types.reference(Item)) }) const folder = Folder.create({ id: "folder 1", objects: { "item 1": { id: "item 1", name: "item name 1" } }, hovers: ["item 1"] }) const snapshot = JSON.parse(JSON.stringify(getSnapshot(folder))) expect(snapshot).toEqual({ id: "folder 1", objects: { "item 1": { id: "item 1", name: "item name 1" } }, hovers: ["item 1"] }) snapshot.hovers = [] applySnapshot(folder, snapshot) expect(getSnapshot(folder)).toEqual({ id: "folder 1", objects: { "item 1": { id: "item 1", name: "item name 1" } }, hovers: [] }) snapshot.hovers = ["item 1"] applySnapshot(folder, snapshot) expect(getSnapshot(folder)).toEqual({ id: "folder 1", objects: { "item 1": { id: "item 1", name: "item name 1" } }, hovers: ["item 1"] }) }) test("array of references should work fine", () => { const B = types.model("Block", { id: types.identifier }) const S = types .model("Store", { blocks: types.array(B), blockRefs: types.array(types.reference(B)) }) .actions(self => { return { order() { const res = self.blockRefs.slice() self.blockRefs.replace([res[1], res[0]]) } } }) const a = S.create({ blocks: [{ id: "1" }, { id: "2" }], blockRefs: ["1", "2"] }) a.order() expect(a.blocks[0].id).toBe("1") expect(a.blockRefs[0].id).toBe("2") }) test("should serialize references correctly", () => { const M = types.model({ id: types.identifierNumber }) const S = types.model({ mies: types.map(M), ref: types.maybe(types.reference(M)) }) const s = S.create({ mies: { 7: { id: 7 } } }) unprotect(s) expect(Array.from(s.mies.keys())).toEqual(["7"]) expect(s.mies.get("7")!.id).toBe(7) expect(s.mies.get(7 as any)).toBe(s.mies.get("7")!) // maps automatically normalizes the key s.mies.put({ id: 8 }) expect(Array.from(s.mies.keys())).toEqual(["7", "8"]) s.ref = 8 as any expect(s.ref!.id).toBe(8) // resolved from number expect(getSnapshot(s).ref).toBe(8) // ref serialized as number s.ref = "7" as any // resolved from string expect(s.ref!.id).toBe(7) // resolved from string expect(getSnapshot(s).ref).toBe("7") // ref serialized as string (number would be ok as well) s.ref = s.mies.get("8")! expect(s.ref.id).toBe(8) // resolved from instance expect(getSnapshot(s).ref).toBe(8) // ref serialized as number s.ref = "9" as any // unresolvable expect(getSnapshot(s).ref).toBe("9") // snapshot preserved as it was unresolvable s.mies.set(9 as any, { id: 9 }) expect(Array.from(s.mies.keys())).toEqual(["7", "8", "9"]) expect(s.mies.get("9")!.id).toBe(9) expect(getSnapshot(s).ref).toBe("9") // ref serialized as string (number would be ok as well) }) test("#1052 - Reference returns destroyed model after subtree replacing", () => { const Todo = types.model("Todo", { id: types.identifierNumber, title: types.string }) const Todos = types.model("Todos", { items: types.array(Todo) }) const Store = types .model("Store", { todos: Todos, last: types.maybe(types.reference(Todo)), lastWithId: types.maybe(types.reference(Todo)), counter: -1 }) .actions(self => ({ load() { self.counter++ self.todos = Todos.create({ items: [ { id: 1, title: "Get Coffee " + self.counter }, { id: 2, title: "Write simpler code " + self.counter } ] }) }, select(todo: Instance) { self.last = todo self.lastWithId = todo.id as any } })) const store = Store.create({ todos: {} }) store.load() expect(store.last).toBeUndefined() expect(store.lastWithId).toBeUndefined() const reactionFn = jest.fn() const reactionDisposer = reaction(() => store.last, reactionFn) const reactionFn2 = jest.fn() const reactionDisposer2 = reaction(() => store.lastWithId, reactionFn2) try { store.select(store.todos.items[0]) expect(isAlive(store.last!)).toBe(true) expect(isObservable(store.last)).toBe(true) expect(reactionFn).toHaveBeenCalledTimes(1) expect(store.last!.title).toBe("Get Coffee 0") expect(isAlive(store.lastWithId!)).toBe(true) expect(isObservable(store.lastWithId)).toBe(true) expect(reactionFn2).toHaveBeenCalledTimes(1) expect(store.lastWithId!.title).toBe("Get Coffee 0") store.load() expect(isAlive(store.last!)).toBe(true) expect(isObservable(store.last)).toBe(true) expect(reactionFn).toHaveBeenCalledTimes(2) expect(store.last!.title).toBe("Get Coffee 1") expect(isAlive(store.lastWithId!)).toBe(true) expect(isObservable(store.lastWithId)).toBe(true) expect(reactionFn2).toHaveBeenCalledTimes(2) expect(store.lastWithId!.title).toBe("Get Coffee 1") } finally { reactionDisposer() reactionDisposer2() } }) test("#1080 - does not crash trying to resolve a reference to a destroyed+recreated model", () => { configure({ useProxies: "never" }) const Branch = types.model("Branch", { id: types.identifierNumber, name: types.string }) const User = types.model("User", { id: types.identifierNumber, email: types.maybeNull(types.string), branches: types.maybeNull(types.array(Branch)) }) const BranchStore = types .model("BranchStore", { activeBranch: types.maybeNull(types.reference(Branch)) }) .actions(self => ({ setActiveBranch(branchId: any) { self.activeBranch = branchId } })) const RootStore = types .model("RootStore", { user: types.maybeNull(User), branchStore: types.maybeNull(BranchStore) }) .actions(self => ({ setUser(snapshot: typeof userSnapshot) { self.user = cast(snapshot) }, setBranchStore(snapshot: typeof branchStoreSnapshot) { self.branchStore = cast(snapshot) }, destroyUser() { destroy(self.user!) }, destroyBranchStore() { destroy(self.branchStore!) } })) const userSnapshot = { id: 1, email: "test@test.com", branches: [ { id: 1, name: "Branch 1" }, { id: 2, name: "Branch 2" } ] } const branchStoreSnapshot = {} const rootStore = RootStore.create({ user: userSnapshot, branchStore: branchStoreSnapshot }) rootStore.branchStore!.setActiveBranch(1) expect(rootStore.branchStore!.activeBranch).toEqual({ id: 1, name: "Branch 1" }) rootStore.destroyUser() rootStore.destroyBranchStore() rootStore.setUser(userSnapshot) rootStore.setBranchStore(branchStoreSnapshot) rootStore.branchStore!.setActiveBranch(2) expect(rootStore.branchStore!.activeBranch).toEqual({ id: 2, name: "Branch 2" }) }) test("tryReference / isValidReference", () => { const Todo = types.model({ id: types.identifier }) const TodoStore = types .model({ todos: types.array(Todo), ref1: types.maybe(types.reference(Todo)), ref2: types.maybeNull(types.reference(Todo)), ref3: types.maybe(types.reference(Todo)) }) .actions(self => ({ clearRef3() { self.ref3 = undefined }, afterCreate() { addDisposer( self, reaction( () => isValidReference(() => self.ref3), valid => { if (!valid) { this.clearRef3() } }, { fireImmediately: true } ) ) } })) const store = TodoStore.create({ todos: [{ id: "1" }, { id: "2" }, { id: "3" }] }) expect(tryReference(() => store.ref1)).toBeUndefined() expect(tryReference(() => store.ref2)).toBeUndefined() expect(isValidReference(() => store.ref1)).toBe(false) expect(isValidReference(() => store.ref2)).toBe(false) unprotect(store) store.ref1 = store.todos[0] store.ref2 = store.todos[1] store.ref3 = store.todos[2] expect(isStateTreeNode(store.ref1)).toBe(true) expect(isStateTreeNode(store.ref2)).toBe(true) expect(tryReference(() => store.ref1)).toBeDefined() expect(tryReference(() => store.ref2)).toBeDefined() expect(isValidReference(() => store.ref1)).toBe(true) expect(isValidReference(() => store.ref2)).toBe(true) store.todos = cast([]) expect(tryReference(() => store.ref1)).toBeUndefined() expect(tryReference(() => store.ref2)).toBeUndefined() expect(isValidReference(() => store.ref1)).toBe(false) expect(isValidReference(() => store.ref2)).toBe(false) // the reaction should have triggered and set this to undefined expect(store.ref3).toBeUndefined() expect(() => tryReference(() => 5 as any)).toThrow( "The reference to be checked is not one of node, null or undefined" ) expect(() => isValidReference(() => 5 as any)).toThrow( "The reference to be checked is not one of node, null or undefined" ) }) test("#1162 - reference to union", () => { const M1 = types.model({ id: types.identifier, type: types.string, sum: types.string }) const M2 = types.model({ id: types.identifier, type: types.string, data: types.string }) const AnyModel = types.union( { dispatcher(snapshot) { switch (snapshot.type) { case "type1": return M1 case "type2": return M2 default: throw new Error() } } }, M1, M2 ) const Store = types.model({ arr: types.array(AnyModel), selected: types.reference(AnyModel) }) const s = Store.create({ selected: "num1", arr: [ { id: "num1", type: "type1", sum: "1" }, { id: "num2", type: "type1", sum: "2" }, { id: "num3", type: "type2", data: "3" } ] }) unprotect(s) expect(s.selected.id).toBe("num1") expect(s.selected.type).toBe("type1") expect((s.selected as Instance).sum).toBe("1") s.selected = "num2" as any expect(s.selected.id).toBe("num2") expect(s.selected.type).toBe("type1") expect((s.selected as Instance).sum).toBe("2") s.selected = "num3" as any expect(s.selected.id).toBe("num3") expect(s.selected.type).toBe("type2") expect((s.selected as Instance).data).toBe("3") }) ================================================ FILE: __tests__/core/refinement.test.ts ================================================ import { getSnapshot, types } from "../../src" import { expect, test } from "bun:test" test("it should allow if type and predicate is correct", () => { const Factory = types.model({ number: types.refinement( "positive number", types.optional(types.number, 0), s => typeof s === "number" && s >= 0 ) }) const doc = Factory.create({ number: 42 }) expect(getSnapshot(doc)).toEqual({ number: 42 }) }) if (process.env.NODE_ENV !== "production") { test("it should throw if a correct type with failing predicate is given", () => { const Factory = types.model({ number: types.refinement( "positive number", types.optional(types.number, 0), s => typeof s === "number" && s >= 0 ) }) expect(() => { Factory.create({ number: "givenStringInstead" } as any) }).toThrow( `[mobx-state-tree] Error while converting \`{\"number\":\"givenStringInstead\"}\` to \`AnonymousModel\`:\n\n at path \"/number\" value \`\"givenStringInstead\"\` is not assignable to type: \`positive number\` (Value is not a number).` ) expect(() => { Factory.create({ number: -4 }) }).toThrow( `[mobx-state-tree] Error while converting \`{\"number\":-4}\` to \`AnonymousModel\`:\n\n at path \"/number\" value \`-4\` is not assignable to type: \`positive number\` (Value does not respect the refinement predicate).` ) }) test("it should throw custom error message with failing predicate is given", () => { const Factory = types.model({ number: types.refinement( types.optional(types.number, 0), s => typeof s === "number" && s >= 0, s => "A positive number was expected" ) }) expect(() => { Factory.create({ number: "givenStringInstead" } as any) }).toThrow( `[mobx-state-tree] Error while converting \`{\"number\":\"givenStringInstead\"}\` to \`AnonymousModel\`:\n\n at path \"/number\" value \`\"givenStringInstead\"\` is not assignable to type: \`number\` (Value is not a number).` ) expect(() => { Factory.create({ number: -4 }) }).toThrow( `[mobx-state-tree] Error while converting \`{\"number\":-4}\` to \`AnonymousModel\`:\n\n at path "/number" value \`-4\` is not assignable to type: \`number\` (A positive number was expected).` ) }) } ================================================ FILE: __tests__/core/reflection.test.ts ================================================ import { types, getMembers, getPropertyMembers, IAnyStateTreeNode, getType, IAnyModelType, IModelReflectionData, IModelReflectionPropertiesData, flow } from "../../src" import { expect, test } from "bun:test" const User = types.model("User", { id: types.identifier, name: types.string }) const Model = types .model({ isPerson: false, users: types.optional(types.map(User), {}), dogs: types.array(User), user: types.maybe(types.late(() => User)) }) .volatile(self => ({ volatileProperty: { propName: "halo" } })) .actions(self => { function actionName() { return 1 } return { actionName, generatorAction: flow(function* generatorAction() { const promise = new Promise(resolve => { resolve(true) }) yield promise }) } }) .views(self => ({ get viewName() { return 1 } })) function expectPropertyMembersToMatchMembers( propertyMembers: IModelReflectionPropertiesData, members: IModelReflectionData ) { expect(propertyMembers).toEqual({ name: members.name, properties: members.properties }) } test("reflection - model", () => { const node = Model.create() const reflection = getMembers(node) expect(reflection.name).toBe("AnonymousModel") expect(reflection.actions.includes("actionName")).toBe(true) expect(reflection.actions.includes("generatorAction")).toBe(true) expect(reflection.flowActions.includes("generatorAction")).toBe(true) expect(reflection.flowActions.includes("actionName")).toBe(false) expect(reflection.views.includes("viewName")).toBe(true) expect(reflection.views.includes("actionName")).toBe(false) expect(reflection.volatile.includes("volatileProperty")).toBe(true) expect(!!reflection.properties.users).toBe(true) expect(!!reflection.properties.isPerson).toBe(true) const typeReflection = getPropertyMembers(Model) expectPropertyMembersToMatchMembers(typeReflection, reflection) const reflection2 = getPropertyMembers(node) expectPropertyMembersToMatchMembers(reflection2, reflection) }) test("reflection - map", () => { const node = Model.create({ users: { "1": { id: "1", name: "Test" } } }) const node2 = node.users.get("1")! const reflection = getMembers(node2) expect(reflection.name).toBe("User") expect(!!reflection.properties.id).toBe(true) expect(!!reflection.properties.name).toBe(true) const typeReflection = getPropertyMembers(getType(node2) as IAnyModelType) expectPropertyMembersToMatchMembers(typeReflection, reflection) const reflection2 = getPropertyMembers(node2) expectPropertyMembersToMatchMembers(reflection2, reflection) }) test("reflection - array", () => { const node = Model.create({ dogs: [{ id: "1", name: "Test" }] }) const node2 = node.dogs[0] const reflection = getMembers(node2) expect(!!reflection.properties.id).toBe(true) expect(!!reflection.properties.name).toBe(true) const typeReflection = getPropertyMembers(getType(node2) as IAnyModelType) expectPropertyMembersToMatchMembers(typeReflection, reflection) const reflection2 = getPropertyMembers(node2) expectPropertyMembersToMatchMembers(reflection2, reflection) }) test("reflection - late", () => { const node = Model.create({ user: { id: "5", name: "Test" } }) const empty: IAnyStateTreeNode = {} const reflection = getMembers(node.user || empty) const keys = Object.keys(reflection.properties || {}) expect(keys.includes("name")).toBe(true) expect(reflection.properties.name.describe()).toBe("string") }) if (process.env.NODE_ENV !== "production") { test("reflection - throw on non model node for getMembers", () => { const node = Model.create({ users: { "1": { id: "1", name: "Test" } } }) expect(() => (node.users ? getMembers(node.users) : {})).toThrow() }) test("reflection - throw on non model type/node for getMembers", () => { expect(() => getPropertyMembers(types.array(types.number) as any)).toThrow() const node = Model.create({ users: { "1": { id: "1", name: "Test" } } }) expect(() => getPropertyMembers(node.users)).toThrow() }) } test("reflection - can retrieve property names", () => { const node = Model.create() const reflection = getMembers(node) const keys = Object.keys(reflection.properties) expect(keys.includes("users")).toBe(true) expect(keys.includes("isPerson")).toBe(true) }) test("reflection - property contains type", () => { const TestModel = types.model({ string: types.string, optional: false }) const node = TestModel.create({ string: "hello" }) const reflection = getMembers(node) expect(reflection.properties.string).toBe(types.string) expect(reflection.properties.optional).toMatchObject(types.optional(types.boolean, false)) }) test("reflection - members chained", () => { const ChainedModel = types .model({ isPerson: false }) .actions(self => { return { actionName() { return 1 } } }) .actions(self => { return { anotherAction() { return 1 } } }) .actions(self => { function flowActionName() { return 1 } return { flowActionName, generatorAction: flow(function* generatorAction() { const promise = new Promise(resolve => { resolve(true) }) yield promise }) } }) .views(self => ({ get viewName() { return 1 } })) .views(self => ({ anotherView(prop: string) { return 1 } })) const node = ChainedModel.create() const reflection = getMembers(node) const keys = Object.keys(reflection.properties || {}) expect(keys.includes("isPerson")).toBe(true) expect(reflection.actions.includes("actionName")).toBe(true) expect(reflection.actions.includes("anotherAction")).toBe(true) expect(reflection.actions.includes("flowActionName")).toBe(true) expect(reflection.actions.includes("generatorAction")).toBe(true) expect(reflection.flowActions.includes("generatorAction")).toBe(true) expect(reflection.flowActions.includes("flowActionName")).toBe(false) expect(reflection.views.includes("viewName")).toBe(true) expect(reflection.views.includes("anotherView")).toBe(true) expect(reflection.views.includes("actionName")).toBe(false) expect(reflection.views.includes("anotherAction")).toBe(false) expect(reflection.views.includes("flowActionName")).toBe(false) }) test("reflection - conditionals respected", () => { let swap = true const ConditionalModel = types .model({ isPerson: false }) .actions(self => ({ actionName0() { return 1 } })) .actions((self): { actionName1(): number } | { actionName2(): number } => { if (swap) { return { actionName1() { return 1 } } } else { return { actionName2() { return 1 } } } }) .views(self => { if (swap) { return { get view1() { return 1 } } } else { return { get view2() { return 1 } } } }) // swap true const node = ConditionalModel.create() const reflection = getMembers(node) expect(reflection.actions.includes("actionName0")).toBe(true) expect(reflection.actions.includes("actionName1")).toBe(true) expect(reflection.actions.includes("actionName2")).toBe(false) expect(reflection.views.includes("view1")).toBe(true) expect(reflection.views.includes("view2")).toBe(false) swap = false const node2 = ConditionalModel.create() const reflection2 = getMembers(node2) expect(reflection.actions.includes("actionName0")).toBe(true) expect(reflection2.actions.includes("actionName1")).toBe(false) expect(reflection2.actions.includes("actionName2")).toBe(true) expect(reflection2.views.includes("view1")).toBe(false) expect(reflection2.views.includes("view2")).toBe(true) }) ================================================ FILE: __tests__/core/snapshotProcessor.test.ts ================================================ import { observable } from "mobx" import { types, getSnapshot, unprotect, cast, detach, clone, SnapshotIn, getNodeId, Instance, onSnapshot } from "../../src" import { describe, expect, jest, test } from "bun:test" describe("snapshotProcessor", () => { describe("over a model type", () => { const M = types.model({ x: types.string }) test("no processors", () => { const Model = types.model({ m: types.snapshotProcessor(M, {}) }) const model = Model.create({ m: { x: "hi" } }) unprotect(model) expect(model.m.x).toBe("hi") expect(getSnapshot(model).m.x).toBe("hi") // reconciliation model.m = { x: "ho" } expect(model.m.x).toBe("ho") expect(getSnapshot(model).m.x).toBe("ho") }) test("pre processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: { x: number }) { return { ...sn, x: String(sn.x) } } }) }) const model = Model.create({ m: { x: 5 } }) unprotect(model) expect(model.m.x).toBe("5") expect(getSnapshot(model).m.x).toBe("5") // reconciliation model.m = cast({ x: 6 }) expect(model.m.x).toBe("6") expect(getSnapshot(model).m.x).toBe("6") }) test("post processor", () => { let model: Instance const Model = types.model({ m: types.snapshotProcessor(M, { postProcessor(sn, node): { x: number; val?: string } { expect(node).toBeTruthy() return { ...sn, x: Number(sn.x), val: node.x } } }) }) model = Model.create({ m: { x: "5" } }) unprotect(model) expect(model.m.x).toBe("5") expect(getSnapshot(model).m.x).toBe(5) expect(getSnapshot(model).m.val).toBe("5") // reconciliation model.m = cast({ x: "6" }) expect(model.m.x).toBe("6") expect(getSnapshot(model).m.x).toBe(6) expect(getSnapshot(model).m.val).toBe("6") }) test("post processor that observes other observables recomputes when they change", () => { let model: Instance const atom = observable.box("foo") const Model = types.model({ m: types.snapshotProcessor(M, { postProcessor(sn, node): { x: number; val: string } { return { ...sn, x: Number(sn.x), val: atom.get() } } }) }) model = Model.create({ m: { x: "5" } }) const newSnapshot = jest.fn() onSnapshot(model, newSnapshot) expect(getSnapshot(model).m.val).toBe("foo") atom.set("bar") expect(getSnapshot(model).m.val).toBe("bar") expect(newSnapshot).toHaveBeenCalledTimes(1) }) test("pre and post processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: { x: number }) { return { ...sn, x: String(sn.x) } }, postProcessor(sn): { x: number } { return { ...sn, x: Number(sn.x) } } }) }) const model = Model.create({ m: { x: 5 } }) unprotect(model) expect(model.m.x).toBe("5") expect(getSnapshot(model).m.x).toBe(5) // reconciliation model.m = cast({ x: 6 }) expect(model.m.x).toBe("6") expect(getSnapshot(model).m.x).toBe(6) // cloning expect(getSnapshot(clone(model.m)).x).toBe(6) }) }) describe("over a literal type", () => { const M = types.string test("no processors", () => { const Model = types.model({ m: types.snapshotProcessor(M, {}) }) const model = Model.create({ m: "hi" }) unprotect(model) expect(model.m).toBe("hi") expect(getSnapshot(model).m).toBe("hi") // reconciliation model.m = "ho" expect(model.m).toBe("ho") expect(getSnapshot(model).m).toBe("ho") }) test("pre processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: number) { return String(sn) } }) }) const model = Model.create({ m: 5 }) unprotect(model) expect(model.m).toBe("5") expect(getSnapshot(model).m).toBe("5") // reconciliation model.m = 6 as any expect(model.m).toBe("6") expect(getSnapshot(model).m).toBe("6") }) test("post processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { postProcessor(sn, node): number { expect(node).toMatch(/5|6/) return Number(sn) } }) }) const model = Model.create({ m: "5" }) unprotect(model) expect(model.m).toBe("5") expect(getSnapshot(model).m).toBe(5) // reconciliation model.m = "6" expect(model.m).toBe("6") expect(getSnapshot(model).m).toBe(6) }) test("pre and post processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: number) { return String(sn) }, postProcessor(sn): number { return Number(sn) } }) }) const model = Model.create({ m: 5 }) unprotect(model) expect(model.m).toBe("5") expect(getSnapshot(model).m).toBe(5) // reconciliation model.m = "6" expect(model.m).toBe("6") expect(getSnapshot(model).m).toBe(6) // cloning expect(getSnapshot(clone(model)).m).toBe(6) }) }) describe("over an array type", () => { const M = types.array(types.string) test("no processors", () => { const Model = types.model({ m: types.snapshotProcessor(M, {}) }) const model = Model.create({ m: ["hi"] }) unprotect(model) expect(model.m[0]).toBe("hi") expect(getSnapshot(model).m[0]).toBe("hi") // reconciliation model.m = cast(["ho"]) expect(model.m[0]).toBe("ho") expect(getSnapshot(model).m[0]).toBe("ho") }) test("pre processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: number[]) { return sn.map(n => String(n)) } }) }) const model = Model.create({ m: [5] }) unprotect(model) expect(model.m[0]).toBe("5") expect(getSnapshot(model).m[0]).toBe("5") // reconciliation model.m = cast([6]) expect(model.m[0]).toBe("6") expect(getSnapshot(model).m[0]).toBe("6") }) test("post processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { postProcessor(sn, node): number[] { expect(node).toBeDefined() expect(node.length).toEqual(1) return sn.map(n => Number(n)) } }) }) const model = Model.create({ m: ["5"] }) unprotect(model) expect(model.m[0]).toBe("5") expect(getSnapshot(model).m[0]).toBe(5) // reconciliation model.m = cast(["6"]) expect(model.m[0]).toBe("6") expect(getSnapshot(model).m[0]).toBe(6) }) test("pre and post processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: number[]) { return sn.map(n => String(n)) }, postProcessor(sn): number[] { return sn.map(n => Number(n)) } }) }) const model = Model.create({ m: [5] }) unprotect(model) expect(model.m[0]).toBe("5") expect(getSnapshot(model).m[0]).toBe(5) // reconciliation model.m = cast([6]) expect(model.m[0]).toBe("6") expect(getSnapshot(model).m[0]).toBe(6) // cloning expect(getSnapshot(clone(model.m))[0]).toBe(6) }) }) describe("over a map type", () => { const M = types.map(types.string) test("no processors", () => { const Model = types.model({ m: types.snapshotProcessor(M, {}) }) const model = Model.create({ m: { x: "hi" } }) unprotect(model) expect(model.m.get("x")).toBe("hi") expect(getSnapshot(model).m.x).toBe("hi") // reconciliation model.m.set("x", "ho") expect(model.m.get("x")).toBe("ho") expect(getSnapshot(model).m.x).toBe("ho") }) test("pre processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: { x: number }) { return { ...sn, x: String(sn.x) } } }) }) const model = Model.create({ m: { x: 5 } }) unprotect(model) expect(model.m.get("x")).toBe("5") expect(getSnapshot(model).m.x).toBe("5") // reconciliation model.m = cast({ x: 6 }) expect(model.m.get("x")).toBe("6") expect(getSnapshot(model).m.x).toBe("6") }) test("post processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { postProcessor(sn, node): { x: number } { expect(node.size).toBe(1) return { ...sn, x: Number(sn.x) } } }) }) const model = Model.create({ m: { x: "5" } }) unprotect(model) expect(model.m.get("x")).toBe("5") expect(getSnapshot(model).m.x).toBe(5) // reconciliation model.m = cast({ x: "6" }) expect(model.m.get("x")).toBe("6") expect(getSnapshot(model).m.x).toBe(6) }) test("pre and post processor", () => { const Model = types.model({ m: types.snapshotProcessor(M, { preProcessor(sn: { x: number }) { return { ...sn, x: String(sn.x) } }, postProcessor(sn): { x: number } { return { ...sn, x: Number(sn.x) } } }) }) const model = Model.create({ m: { x: 5 } }) unprotect(model) expect(model.m.get("x")).toBe("5") expect(getSnapshot(model).m.x).toBe(5) // reconciliation model.m = cast({ x: 6 }) expect(model.m.get("x")).toBe("6") expect(getSnapshot(model).m.x).toBe(6) // cloning expect(getSnapshot(clone(model.m)).x).toBe(6) }) }) test("chained transforms", () => { const TL = types.snapshotProcessor(types.string, { preProcessor(sn: string) { return sn.trimLeft() }, postProcessor(sn): string { return "_" + sn } }) const TB = types.snapshotProcessor(TL, { preProcessor(sn: string) { return sn.trimRight() }, postProcessor(sn): string { return sn + "_" } }) const M = types.model({ name: TB }) const t = TB.create(" hello ") expect(t).toBe("hello") const m = M.create({ name: " hello " }) expect(m.name).toBe("hello") expect(getSnapshot(m).name).toBe("_hello_") }) describe("moving nodes around with a pre-processor", () => { const Task = types.model("Task", { x: types.number }) const Store = types.model({ a: types.array( types.snapshotProcessor( Task, { preProcessor(sn: { x: string }) { return { x: Number(sn.x) } } }, "PTask" ) ), b: types.array(Task) }) test("moving from a to b", () => { const s = Store.create({ a: [{ x: "1" }] }) unprotect(s) const n = s.a[0] detach(n) expect(s.a.length).toBe(0) expect(getSnapshot(n)).toEqual({ x: 1 }) s.b.push(n) expect(s.b.length).toBe(1) expect(getSnapshot(s.b)).toEqual([{ x: 1 }]) }) test("moving from b to a", () => { const s = Store.create({ b: [{ x: 1 }] }) unprotect(s) const n = s.b[0] detach(n) expect(s.b.length).toBe(0) expect(getSnapshot(n)).toEqual({ x: 1 }) s.a.push(n) expect(s.a.length).toBe(1) expect(getSnapshot(s.a)).toEqual([{ x: 1 }]) }) }) describe("moving nodes around with a post-processor", () => { const Task = types.model({ x: types.number }) const Store = types.model({ a: types.array( types.snapshotProcessor(Task, { postProcessor(sn): { x: string } { return { x: String(sn.x) } } }) ), b: types.array(Task) }) test("moving from a to b", () => { const s = Store.create({ a: [{ x: 1 }] }) unprotect(s) const n = s.a[0] detach(n) expect(s.a.length).toBe(0) expect(getSnapshot(n)).toEqual({ x: "1" }) s.b.push(n) expect(s.b.length).toBe(1) // @ts-expect-error - post processor gets applied here and messes up the types expect(getSnapshot(s.b)).toEqual([{ x: "1" }]) }) test("moving from b to a", () => { const s = Store.create({ b: [{ x: 1 }] }) unprotect(s) const n = s.b[0] detach(n) expect(s.b.length).toBe(0) expect(getSnapshot(n)).toEqual({ x: 1 }) s.a.push(n) expect(s.a.length).toBe(1) expect(getSnapshot(s.a)).toEqual([{ x: "1" }]) }) }) describe("assigning instances works", () => { const Todo = types.model("Todo", { id: types.identifier }) const TodoWithProcessor = types.snapshotProcessor(Todo, { preProcessor(snapshot: { id: string }) { return snapshot } }) const Store = types .model("TodoStore", { todos: types.map(TodoWithProcessor), instance: types.optional(TodoWithProcessor, { id: "new" }) }) .actions(self => ({ addTodo(todo: { id: string }) { self.todos.put(todo) }, setInstance(next: { id: string }) { self.instance = next } })) test("using instances in maps work", () => { const store = Store.create() const todo = TodoWithProcessor.create({ id: "map" }) store.addTodo(todo) expect(store.todos.size).toBe(1) expect(getSnapshot(store.todos)).toEqual({ map: { id: "map" } }) }) test("using instances as values works", () => { const store = Store.create() const todo = TodoWithProcessor.create({ id: "map" }) store.setInstance(todo) expect(store.instance).toBe(todo) }) test("using the non processed type in place of the processed one works", () => { const store = Store.create() const todo = Todo.create({ id: "map" }) store.setInstance(todo) expect(store.instance).toBe(todo) }) test("using the processed type in place of the non processed one works", () => { const store = types .model("Store", { instance: Todo }) .actions(self => ({ setInstance(next: { id: string }) { self.instance = next } })) .create({ instance: { id: "new" } }) const todo = TodoWithProcessor.create({ id: "map" }) store.setInstance(todo) expect(store.instance).toBe(todo) }) }) test("cached initial snapshots are ok", () => { const M2 = types.snapshotProcessor(types.model({ x: types.number }), { preProcessor(sn: { x: number }) { return { ...sn, x: 0 } } }) const M1 = types.model({ m2: M2 }) const M = types.model({ m1: M1 }) const m = M.create({ m1: { m2: { x: 10 } } }) expect(getSnapshot(m)).toEqual({ m1: { m2: { x: 0 } } }) }) test("works with IType.is", () => { const Model = types.model({ x: types.number }) const model = Model.create({ x: 1 }) expect(Model.is(model)).toBe(true) expect(Model.is({ x: 1 })).toBe(true) const ProcessedModel = types.snapshotProcessor(Model, { preProcessor(sn: { y: number }) { const copy = { ...sn, x: sn.y } // @ts-ignore delete copy.y return copy }, postProcessor(sn: { x: number }) { const copy = { ...sn, y: sn.x } // @ts-ignore delete copy.x return copy } }) const processedModel = ProcessedModel.create({ y: 1 }) expect(ProcessedModel.is(processedModel)).toBe(true) expect(ProcessedModel.is({ y: 1 })).toBe(true) expect(ProcessedModel.is(Model)).toBe(false) }) test(".is checks instances against the underlying type", () => { const ModelA = types.model({ x: types.number }) const ModelB = types.model({ x: types.number }) const modelA = ModelA.create({ x: 1 }) const modelB = ModelB.create({ x: 2 }) // despite having the same snapshot type, .is is false for the instance of one against the other because they are not the same type expect(ModelA.is(modelA)).toBe(true) expect(ModelB.is(modelA)).toBe(false) expect(ModelA.is(modelB)).toBe(false) expect(ModelB.is(modelB)).toBe(true) const ProcessedModel = types.snapshotProcessor(ModelA, {}) const processedModel = ProcessedModel.create({ x: 3 }) expect(ProcessedModel.is(processedModel)).toBe(true) expect(ModelA.is(processedModel)).toBe(true) expect(ModelB.is(processedModel)).toBe(false) expect(ProcessedModel.is(modelA)).toBe(true) expect(ProcessedModel.is(modelB)).toBe(false) }) describe("1776 - reconciliation in an array", () => { test("model with transformed property is reconciled", () => { const SP = types.snapshotProcessor( types.model({ id: types.identifier, x: types.number }), { preProcessor(sn: { id: string; y: number }) { if ("x" in sn) { // Ensure snapshot don't run through preprocessor twice throw new Error("sn has already been preprocessed") } return { id: sn.id, x: sn.y } } } ) const Store = types.model({ items: types.array(SP) }).actions(self => ({ setItems(items: SnapshotIn[]) { self.items = cast(items) } })) const store = Store.create({ items: [{ id: "1", y: 0 }] }) const oldNodeId = getNodeId(store.items[0]) store.setItems([{ id: "1", y: 1 }]) expect(getNodeId(store.items[0])).toBe(oldNodeId) }) test("model with transformed identifier attribute is reconciled", () => { const SP = types.snapshotProcessor( types.model({ id: types.identifier }), { preProcessor(sn: { foo: string }) { return { id: sn.foo } } } ) const Store = types.model({ items: types.array(SP) }).actions(self => ({ setItems(items: SnapshotIn[]) { self.items = cast(items) } })) const store = Store.create({ items: [{ foo: "1" }] }) const oldNodeId = getNodeId(store.items[0]) store.setItems([{ foo: "1" }]) expect(getNodeId(store.items[0])).toBe(oldNodeId) }) }) describe("single node reconcilication", () => { test("model with transformed property is reconciled", () => { const SP = types.snapshotProcessor( types.model({ id: types.identifier, x: types.number }), { preProcessor(sn: { id: string; y: number }) { if ("x" in sn) { // Ensure snapshot don't run through preprocessor twice throw new Error("sn has already been preprocessed") } return { id: sn.id, x: sn.y } } } ) const Store = types.model({ item: SP }).actions(self => ({ setItem(item: SnapshotIn) { self.item = cast(item) } })) const store = Store.create({ item: { id: "1", y: 0 } }) const oldNodeId = getNodeId(store.item) store.setItem({ id: "1", y: 1 }) expect(getNodeId(store.item)).toBe(oldNodeId) expect(store.item.x).toBe(1) }) test("model with transformed identifier property is reconciled", () => { const SP = types.snapshotProcessor( types.model({ id: types.identifier }), { preProcessor(sn: { foo: string }) { return { id: sn.foo } } } ) const Store = types.model({ item: SP }).actions(self => ({ setItem(item: SnapshotIn) { self.item = cast(item) } })) const store = Store.create({ item: { foo: "1" } }) const oldNodeId = getNodeId(store.item) store.setItem({ foo: "1" }) expect(getNodeId(store.item)).toBe(oldNodeId) expect(store.item.id).toBe("1") }) test("1791 - model wrapped with maybe is reconciled", () => { const SP = types.snapshotProcessor( types.model({ id: types.identifier, x: types.number }), { preProcessor(sn: { id: string; y: number }) { return { id: sn.id, x: sn.y } } } ) const Store = types.model({ item: types.maybe(SP) }).actions(self => ({ setItem(item: SnapshotIn) { self.item = cast(item) } })) const store = Store.create({ item: { id: "1", y: 0 } }) const oldNodeId = getNodeId(store.item!) store.setItem({ id: "1", y: 1 }) expect(getNodeId(store.item!)).toBe(oldNodeId) expect(store.item?.x).toBe(1) }) test("model wrapped with optional is reconciled", () => { const SP = types.snapshotProcessor( types.model({ id: types.identifier, x: types.number }), { preProcessor(sn: { id: string; y: number }) { return { id: sn.id, x: sn.y } } } ) const Store = types .model({ item: types.optional(SP, { id: "1", y: 0 }) }) .actions(self => ({ setItem(item?: SnapshotIn) { self.item = cast(item) } })) const store = Store.create() const oldNodeId = getNodeId(store.item!) expect(store.item?.x).toBe(0) store.setItem({ id: "1", y: 1 }) expect(getNodeId(store.item!)).toBe(oldNodeId) expect(store.item?.x).toBe(1) store.setItem(undefined) expect(getNodeId(store.item!)).toBe(oldNodeId) expect(store.item?.x).toBe(0) }) }) test("1777 - preProcessor wrapped in maybe accepts undefined", () => { const SP = types.snapshotProcessor( types.model({ id: types.identifier, x: types.number }), { preProcessor(sn: { id: string; y: number }) { return { id: sn.id, x: sn.y } } } ) const Store = types.model({ item: types.maybe(SP) }).actions(self => ({ setItem(item?: SnapshotIn) { self.item = cast(item) } })) const store = Store.create() expect(store.item).toBeUndefined() store.setItem({ id: "1", y: 1 }) expect(store.item?.x).toBe(1) store.setItem(undefined) expect(store.item).toBeUndefined() }) test("1849 - Wrapped unions don't cause infinite recursion", () => { const Store = types .model({ prop: types.optional( types.snapshotProcessor( types.union(types.literal("a"), types.literal("b")), {} ), "a" ) }) .actions(self => ({ setProp(prop: typeof self.prop) { self.prop = prop } })) const store = Store.create() expect(store.prop).toBe("a") expect(() => store.setProp("b")).not.toThrow() expect(store.prop).toBe("b") }) if (process.env.NODE_ENV !== "production") { test("it should fail if given incorrect processor", () => { expect(() => { types.model({ m: types.snapshotProcessor(types.number, { postProcessor: {} as any }) }) }).toThrow("[mobx-state-tree] postSnapshotProcessor must be a function") }) } }) ================================================ FILE: __tests__/core/string.test.ts ================================================ import { types } from "../../src" import { Hook, NodeLifeCycle } from "../../src/internal" import { describe, expect, it, test } from "bun:test" describe("types.string", () => { describe("methods", () => { describe("create", () => { describe("with no arguments", () => { if (process.env.NODE_ENV !== "production") { it("should throw an error in development", () => { expect(() => { types.string.create() }).toThrow() }) } }) describe("with a string argument", () => { it("should return a string", () => { const s = types.string.create("foo") expect(typeof s).toBe("string") }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, 1, true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error(), NaN, Infinity ] if (process.env.NODE_ENV !== "production") { testCases.forEach(testCase => { it(`should throw an error when passed ${JSON.stringify(testCase)}`, () => { expect(() => { types.string.create(testCase as any) }).toThrow() }) }) } }) }) describe("describe", () => { it("should return the value 'string'", () => { const description = types.string.describe() expect(description).toBe("string") }) }) describe("getSnapshot", () => { it("should return the value passed in", () => { const s = types.string.instantiate(null, "", {}, "foo") const snapshot = types.string.getSnapshot(s) expect(snapshot).toBe("foo") }) }) describe("getSubtype", () => { it("should return null", () => { const subtype = types.string.getSubTypes() expect(subtype).toBe(null) }) }) describe("instantiate", () => { if (process.env.NODE_ENV !== "production") { describe("with invalid arguments", () => { it("should not throw an error", () => { expect(() => { // @ts-ignore types.string.instantiate() }).not.toThrow() }) }) } describe("with a string argument", () => { it("should return an object", () => { const s = types.string.instantiate(null, "", {}, "foo") expect(typeof s).toBe("object") }) }) }) describe("is", () => { describe("with a string argument", () => { it("should return true", () => { const result = types.string.is("foo") expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, 1, true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error(), NaN, Infinity ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = types.string.is(testCase as any) expect(result).toBe(false) }) }) }) }) describe("isAssignableFrom", () => { describe("with a string argument", () => { it("should return true", () => { const result = types.string.isAssignableFrom(types.string) expect(result).toBe(true) }) }) describe("with argument of different types", () => { const testCases = [ types.Date, types.boolean, types.finite, types.float, types.identifier, types.identifierNumber, types.integer, types.null, types.number, types.undefined ] testCases.forEach(testCase => { it(`should return false when passed ${JSON.stringify(testCase)}`, () => { const result = types.string.isAssignableFrom(testCase as any) expect(result).toBe(false) }) }) }) }) // TODO: we need to test this, but to be honest I'm not sure what the expected behavior is on single string nodes. describe.skip("reconcile", () => {}) describe("validate", () => { describe("with a string argument", () => { it("should return with no validation errors", () => { const result = types.string.validate("foo", []) expect(result).toEqual([]) }) }) describe("with argument of different types", () => { const testCases = [ null, undefined, 1, true, [], function () {}, new Date(), /a/, new Map(), new Set(), Symbol(), new Error(), NaN, Infinity ] testCases.forEach(testCase => { it(`should return with a validation error when passed ${JSON.stringify( testCase )}`, () => { const result = types.string.validate(testCase as any, []) expect(result).toEqual([ { context: [], message: "Value is not a string", value: testCase } ]) }) }) }) }) }) describe("properties", () => { describe("flags", () => { test("return the correct value", () => { const flags = types.string.flags expect(flags).toBe(1) }) }) describe("identifierAttribute", () => { // We don't have a way to set the identifierAttribute on a primitive type, so this should return undefined. test("returns undefined", () => { const identifierAttribute = types.string.identifierAttribute expect(identifierAttribute).toBeUndefined() }) }) describe("isType", () => { test("returns true", () => { const isType = types.string.isType expect(isType).toBe(true) }) }) describe("name", () => { test('returns "string"', () => { const name = types.string.name expect(name).toBe("string") }) }) }) describe("instance", () => { describe("methods", () => { describe("aboutToDie", () => { it("calls the beforeDetach hook", () => { const s = types.string.instantiate(null, "", {}, "foo") let called = false s.registerHook(Hook.beforeDestroy, () => { called = true }) s.aboutToDie() expect(called).toBe(true) }) }) describe("die", () => { it("kills the node", () => { const s = types.string.instantiate(null, "", {}, "foo") s.die() expect(s.isAlive).toBe(false) }) it("should mark the node as dead", () => { const s = types.string.instantiate(null, "", {}, "foo") s.die() expect(s.state).toBe(NodeLifeCycle.DEAD) }) }) describe("finalizeCreation", () => { it("should mark the node as finalized", () => { const s = types.string.instantiate(null, "", {}, "foo") s.finalizeCreation() expect(s.state).toBe(NodeLifeCycle.FINALIZED) }) }) describe("finalizeDeath", () => { it("should mark the node as dead", () => { const s = types.string.instantiate(null, "", {}, "foo") s.finalizeDeath() expect(s.state).toBe(NodeLifeCycle.DEAD) }) }) describe("getReconciliationType", () => { it("should return the correct type", () => { const s = types.string.instantiate(null, "", {}, "foo") const type = s.getReconciliationType() expect(type).toBe(types.string) }) }) describe("getSnapshot", () => { it("should return the value passed in", () => { const s = types.string.instantiate(null, "", {}, "foo") const snapshot = s.getSnapshot() expect(snapshot).toBe("foo") }) }) describe("registerHook", () => { it("should register a hook and call it", () => { const s = types.string.instantiate(null, "", {}, "foo") let called = false s.registerHook(Hook.beforeDestroy, () => { called = true }) s.die() expect(called).toBe(true) }) }) describe("setParent", () => { if (process.env.NODE_ENV !== "production") { describe("with null", () => { it("should throw an error", () => { const s = types.string.instantiate(null, "", {}, "foo") expect(() => { s.setParent(null, "foo") }).toThrow() }) }) describe("with a parent object", () => { it("should throw an error", () => { const Parent = types.model({ child: types.string }) const parent = Parent.create({ child: "foo" }) const s = types.string.instantiate(null, "", {}, "bar") expect(() => { // @ts-ignore s.setParent(parent, "bar") }).toThrow( "[mobx-state-tree] assertion failed: scalar nodes cannot change their parent" ) }) }) } }) }) }) }) ================================================ FILE: __tests__/core/this.test.ts ================================================ import { types } from "../../src" import { isObservableProp, isComputedProp } from "mobx" import { expect, test } from "bun:test" // MWE: disabled test, `this` isn't supposed to work, and afaik nowhere advertised test.skip("this support", () => { const M = types .model({ x: 5 }) .views(self => ({ get x2() { return self.x * 2 }, get x4() { return this.x2 * 2 }, boundTo() { return this }, innerBoundTo() { return () => this }, isThisObservable() { return ( isObservableProp(this, "x2") && isObservableProp(this, "x4") && isObservableProp(this, "localState") && isComputedProp(this, "x2") ) } })) .volatile(self => ({ localState: 3, getLocalState() { return this.localState }, getLocalState2() { return this.getLocalState() * 2 } })) .actions(self => { return { xBy(by: number) { return self.x * by }, setX(x: number) { self.x = x }, setThisX(x: number) { ;(this as any).x = x // this should not affect self.x }, setXBy(x: number) { this.setX(this.xBy(x)) }, setLocalState(x: number) { self.localState = x } } }) const mi = M.create() expect(mi.isThisObservable()).toBe(true) expect(mi.boundTo()).toBe(mi) expect(mi.innerBoundTo()()).toBe(mi) expect(mi.x).toBe(5) mi.setX(6) expect(mi.x).toBe(6) mi.setXBy(2) expect(mi.x).toBe(12) expect(mi.x2).toBe(12 * 2) expect(mi.x4).toBe(12 * 4) expect(mi.xBy(2)).toBe(24) expect(mi.localState).toBe(3) expect(mi.getLocalState()).toBe(3) expect(mi.getLocalState2()).toBe(3 * 2) mi.setLocalState(6) expect(mi.localState).toBe(6) expect(mi.getLocalState()).toBe(6) expect(mi.getLocalState2()).toBe(6 * 2) mi.setLocalState(7) expect(mi.localState).toBe(7) // make sure attempts to modify this (as long as it is not an action) doesn't affect self const oldX = mi.x mi.setThisX(oldX + 1) expect(mi.x).toBe(oldX) }) ================================================ FILE: __tests__/core/type-system.test.ts ================================================ import { describe, expect, test } from "bun:test" import { IAnyType, IType, Instance, ModelPrimitive, ModelPropertiesDeclaration, SnapshotIn, SnapshotOrInstance, SnapshotOut, TypeOfValue, cast, castToSnapshot, getParent, getRoot, getSnapshot, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isReferenceType, isRefinementType, isStateTreeNode, isUnionType, types, unprotect, type IOptionalIType, type ISimpleType } from "../../src" import type { DatePrimitive, IAnyComplexType, IAnyModelType, IArrayType, IMapType, IReferenceType, IUnionType } from "../../src/internal" type DifferingKeys = { [K in keyof ActualT | keyof ExpectedT]: K extends keyof ActualT ? K extends keyof ExpectedT ? Exact extends never ? K : never : K : K }[keyof ActualT | keyof ExpectedT] & string type NotExactErrorMessage = ActualT extends Record ? ExpectedT extends Record ? `Mismatched property: ${DifferingKeys}` : "Expected a non-object type, but received an object" : ExpectedT extends Record ? "Expected an object type, but received a non-object type" : "Types are not exactly equal" type Exact = [A] extends [B] ? ([B] extends [A] ? A : never) : never const assertTypesEqual = ( t: ActualT, u: Exact extends never ? NotExactErrorMessage : ExpectedT ): [ActualT, ExpectedT] => [t, u] as any const _: unknown = undefined const createTestFactories = () => { const Box = types.model({ width: 0, height: 0 }) const Square = types.model({ width: 0, height: 0 }) const Cube = types.model({ width: 0, height: 0, depth: 0 }) return { Box, Square, Cube } } test("it should recognize a valid snapshot", () => { const { Box } = createTestFactories() expect(Box.is({ width: 1, height: 2 })).toEqual(true) expect(Box.is({ width: 1, height: 2, depth: 3 })).toEqual(true) }) test("it should recognize an invalid snapshot", () => { const { Box } = createTestFactories() expect(Box.is({ width: "1", height: "2" })).toEqual(false) }) test("it should check valid nodes as well", () => { const { Box } = createTestFactories() const doc = Box.create() expect(Box.is(doc)).toEqual(true) }) test("it should check invalid nodes as well", () => { const { Box } = createTestFactories() const doc = Box.create() expect( types .model({ anotherAttr: types.number }) .is(doc) ).toEqual(false) }) test("it should do typescript type inference correctly", () => { const A = types .model({ x: types.number, y: types.maybeNull(types.string) }) .views(self => ({ get z(): string { return "hi" } })) .actions(self => { function method() { const x: string = self.z + self.x + self.y anotherMethod(x) } function anotherMethod(x: string) {} return { method, anotherMethod } }) // factory is invokable const a = A.create({ x: 2, y: "7" }) unprotect(a) // property can be used as proper type const z: number = a.x // property can be assigned to correctly a.x = 7 // wrong type cannot be assigned // MANUAL TEST: not ok: a.x = "stuff" // sub factories work const B = types.model({ sub: types.maybe(A) }) const b = B.create() unprotect(b) // sub fields can be reassigned b.sub = A.create({ // MANUAL TEST not ok: z: 4, x: 3 }) // sub fields have proper type b.sub.x = 4 const d: string | null = b.sub.y a.y = null const zz: string = a.z // Manual test not assignable: // a.z = "test" b.sub.method() expect(true).toBe(true) // suppress no asserts warning // snapshots are of the proper type const snapshot = getSnapshot(a) const sx: number = snapshot.x const sy: string | null = snapshot.y expect(sx).toBe(7) expect(sy).toBe(null) }) test("#66 - it should accept superfluous fields", () => { const Item = types.model({ id: types.number, name: types.string }) expect(Item.is({})).toBe(false) expect(Item.is({ id: 3 })).toBe(false) expect(Item.is({ id: 3, name: "" })).toBe(true) expect(Item.is({ id: 3, name: "", description: "" })).toBe(true) const a = Item.create({ id: 3, name: "", description: "bla" } as any) expect((a as any).description).toBe(undefined) }) test("#66 - it should not require defaulted fields", () => { const Item = types.model({ id: types.number, name: types.optional(types.string, "boo") }) expect(Item.is({})).toBe(false) expect(Item.is({ id: 3 })).toBe(true) expect(Item.is({ id: 3, name: "" })).toBe(true) expect(Item.is({ id: 3, name: "", description: "" })).toBe(true) const a = Item.create({ id: 3, description: "bla" } as any) expect((a as any).description).toBe(undefined) expect(a.name).toBe("boo") }) test("#66 - it should be possible to omit defaulted fields", () => { const Item = types.model({ id: types.number, name: "boo" }) expect(Item.is({})).toBe(false) expect(Item.is({ id: 3 })).toBe(true) expect(Item.is({ id: 3, name: "" })).toBe(true) expect(Item.is({ id: 3, name: "", description: "" })).toBe(true) const a = Item.create({ id: 3, description: "bla" } as any) expect((a as any).description).toBe(undefined) expect(a.name).toBe("boo") }) test("#66 - it should pick the correct type of defaulted fields", () => { const Item = types.model({ id: types.number, name: "boo" }) const a = Item.create({ id: 3 }) unprotect(a) expect(a.name).toBe("boo") if (process.env.NODE_ENV !== "production") { expect(() => ((a as any).name = 3)).toThrow( `[mobx-state-tree] Error while converting \`3\` to \`string\`:\n\n value \`3\` is not assignable to type: \`string\` (Value is not a string).` ) } }) test("cannot create factories with null values", () => { expect(() => types.model({ x: null } as any) ).toThrow() }) test("can create factories with maybe primitives", () => { const F = types.model({ x: types.maybeNull(types.string) }) expect(F.is(undefined)).toBe(false) expect(F.is({})).toBe(true) expect(F.is({ x: null })).toBe(true) expect(F.is({ x: "test" })).toBe(true) expect(F.is({ x: 3 })).toBe(false) expect(F.create().x).toBe(null) expect(F.create({ x: undefined }).x).toBe(null) expect(F.create({ x: "" }).x).toBe("") expect(F.create({ x: "3" }).x).toBe("3") }) test("it is possible to refer to a type", () => { const Todo = types .model({ title: types.string }) .actions(self => { function setTitle(v: string) {} return { setTitle } }) function x(): typeof Todo.Type { return Todo.create({ title: "test" }) } const z = x() unprotect(z) z.setTitle("bla") z.title = "bla" // z.title = 3 // Test manual: should give compile error expect(true).toBe(true) // suppress no asserts warning }) test(".Type should not be callable", () => { const Todo = types .model({ title: types.string }) .actions(self => { function setTitle(v: string) {} return { setTitle } }) expect(() => Todo.Type).toThrow() }) test(".SnapshotType should not be callable", () => { const Todo = types .model({ title: types.string }) .actions(self => { function setTitle(v: string) {} return { setTitle } }) expect(() => Todo.SnapshotType).toThrow() }) test("types instances with compatible snapshots should not be interchangeable", () => { const A = types.model("A", {}).actions(self => { function doA() {} return { doA } }) const B = types.model("B", {}).actions(self => { function doB() {} return { doB } }) const C = types.model("C", { x: types.maybe(A) }) expect(A.is({})).toBe(true) expect(A.is(B.create())).toBe(false) // if this yielded true, then `B.create().doA()` should work! expect(A.is(getSnapshot(B.create()))).toBe(true) const c = C.create() unprotect(c) expect(() => { c.x = undefined }).not.toThrow() expect(() => { c.x = cast({}) }).not.toThrow() expect(() => { c.x = A.create() }).not.toThrow() expect(() => { ;(c as any).x = B.create() }).toThrow() }) test("it handles complex types correctly", () => { const Todo = types .model({ title: types.string }) .actions(self => { function setTitle(v: string) {} return { setTitle } }) const Store = types .model({ todos: types.map(Todo) }) .views(self => { function getActualAmount() { return self.todos.size } return { get amount() { return getActualAmount() }, getAmount(): number { return self.todos.size + getActualAmount() } } }) .actions(self => { function setAmount() { const x: number = self.todos.size + self.amount + self.getAmount() } return { setAmount } }) expect(true).toBe(true) // suppress no asserts warning }) if (process.env.NODE_ENV !== "production") { test("it should provide detailed reasons why the value is not applicable", () => { const Todo = types .model({ title: types.string }) .actions(self => { function setTitle(v: string) {} return { setTitle } }) const Store = types .model({ todos: types.map(Todo) }) .views(self => ({ get amount() { return self.todos.size }, getAmount(): number { return self.todos.size + self.todos.size } })) .actions(self => { function setAmount() { const x: number = self.todos.size + self.amount + self.getAmount() } return { setAmount } }) expect(() => Store.create({ todos: { "1": { title: true, setTitle: "hello" } }, amount: 1, getAmount: "hello" } as any) ).toThrow( // MWE: TODO: Ideally (like in MST =< 0.9): // at path "/todos/1/setTitle" value \`"hello"\` is not assignable (Action properties should not be provided in the snapshot). // at path "/amount" value \`1\` is not assignable (Computed properties should not be provided in the snapshot). // at path "/getAmount" value \`"hello"\` is not assignable (View properties should not be provided in the snapshot).` `[mobx-state-tree] Error while converting \`{"todos":{"1":{"title":true,"setTitle":"hello"}},"amount":1,"getAmount":"hello"}\` to \`AnonymousModel\`: at path "/todos/1/title" value \`true\` is not assignable to type: \`string\` (Value is not a string).` ) }) } test("it should type compose correctly", () => { const Car = types .model({ wheels: 3 }) .actions(self => { let connection = null as any as Promise function drive() {} function afterCreate() { connection = Promise.resolve(true) } return { drive, afterCreate } }) const Logger = types .model({ logNode: "test" }) .actions(self => { function log(msg: string) {} return { log } }) const LoggableCar = types.compose(Car, Logger) const x = LoggableCar.create({ wheels: 3, logNode: "test" /* compile error: x: 7 */ }) // x.test() // compile error x.drive() x.log("z") }) test("it should extend {pre,post}ProcessSnapshot on compose", () => { const CompositionTracker = types .model({ composedOf: types.array(types.string), composedWith: types.array(types.string) }) .preProcessSnapshot(snapshot => ({ ...snapshot, composedOf: (snapshot.composedOf || []).concat("CompositionTracker") })) .postProcessSnapshot(snapshot => ({ ...snapshot, composedWith: (snapshot.composedWith || []).concat("WagonTracker") })) const Car = types .model({}) .preProcessSnapshot(snapshot => ({ ...snapshot, composedOf: ((snapshot as any).composedOf || []).concat("Car") })) .postProcessSnapshot(snapshot => ({ ...snapshot, composedWith: ((snapshot as any).composedWith || []).concat("Wagon") })) const Logger = types .model({}) .preProcessSnapshot(snapshot => ({ ...snapshot, composedOf: ((snapshot as any).composedOf || []).concat("CarLogger") })) .postProcessSnapshot(snapshot => ({ ...snapshot, composedWith: ((snapshot as any).composedWith || []).concat("WagonLogger") })) const LoggableCar = types.compose(CompositionTracker, Car, Logger).props({ composedOf: types.array(types.string), composedWith: types.array(types.string) }) const x = LoggableCar.create({}) expect(x.composedOf).toContain("CompositionTracker") expect(x.composedOf).toContain("Car") expect(x.composedOf).toContain("CarLogger") expect(x.composedOf.toJSON()).toEqual(["CompositionTracker", "Car", "CarLogger"]) expect(getSnapshot(x).composedWith).toContain("WagonTracker") expect(getSnapshot(x).composedWith).toContain("Wagon") expect(getSnapshot(x).composedWith).toContain("WagonLogger") expect(getSnapshot(x).composedWith).toEqual(["WagonTracker", "Wagon", "WagonLogger"]) }) test("it should extend types correctly", () => { const Car = types .model({ wheels: 3 }) .actions(self => { function drive() {} return { drive } }) const Logger = types .model("Logger") .props({ logNode: "test" }) .actions(self => { let connection: Promise return { log(msg: string) {}, afterCreate() { connection = Promise.resolve(true) } } }) const LoggableCar = types.compose("LoggableCar", Car, Logger) const x = LoggableCar.create({ wheels: 3, logNode: "test" /* compile error: x: 7 */ }) // x.test() // compile error x.drive() x.log("z") }) test("self referring views", () => { const Car = types.model({ x: 3 }).views(self => { const views = { get triple() { return self.x + views.double }, get double() { return self.x * 2 } } return views }) expect(Car.create().triple).toBe(9) }) test("#922", () => { expect(() => { const Stateable = types.model("Statable", { state: types.optional( types.enumeration("state", ["initalized", "pending", "done", "error"]), "initalized" ) }) const Client = types.model("Client", { id: types.identifierNumber, name: types.string }) const UserClientList = types.compose( "UserClientList", Stateable, types.model({ items: types.array(Client), month: types.optional(types.Date, () => { return new Date() }) }) ) const NonExtendedUserClientList = types.model("NonExtendedUserClientList", { items: types.array(Client), month: types.optional(types.Date, () => { return new Date() }), state: types.optional( types.enumeration("state", ["initalized", "pending", "done", "error"]), "initalized" ) }) const User = types.model("User", { name: types.string, clients: types.optional(UserClientList, () => UserClientList.create({})) }) const NonExtendedUser = types.model("User", { name: types.string, clients: types.optional(NonExtendedUserClientList, () => NonExtendedUserClientList.create({}) ) }) const you = NonExtendedUser.create({ name: "you" }) const me = User.create({ name: "me" }) }).not.toThrow() }) test("#922 - 2", () => { expect(() => { types.optional(types.enumeration("state", ["init", "pending", "done", "error"]), "init") }).not.toThrow() }) test("#932", () => { interface MyInterface { test: string } const MyModel = types.model("MyModel", { myField: types.array(types.frozen()) }) const x = MyModel.create({ myField: [{ test: "stuff" }] }) const a: string = x.myField[0].test }) test("932 - 2", () => { type MyType = string const ModelA = types.model("ModelA", { myField: types.maybe(types.frozen()) }) const x = ModelA.create({}) const y = x.myField // y is string | undefined const ModelA2 = types.model("ModelA", { myField: types.frozen() }) const x2 = ModelA2.create({ myField: "test" // mandatory }) const y2: string = x2.myField // string only }) test("#923", () => { const Foo = types.model("Foo", { name: types.optional(types.string, "") }) const Bar = types.model("Bar", { foos: types.optional(types.array(Foo), []) }) types.optional(types.map(Bar), {}) // Should have no compile error! }) test("snapshot type of reference must be string | number", () => { const M = types.model({ id: types.identifier, a: "bar" }) const R = types.reference(M) const S = types.model({ realM: M, refM: R }) const s = S.create({ realM: { id: "5" }, refM: "5" }) const sn: string | number = getSnapshot(s.refM) }) test("#951", () => { const C = types.model({ a: 123 }) // model as root const ModelWithC = types.model({ c: C }) const modelInstance = ModelWithC.create({ c: C.create() }) // getRoot const modelRoot1 = getRoot(modelInstance.c) const modelCR1: Instance = modelRoot1.c const modelRoot2 = getRoot>(modelInstance.c) const modelCR2: Instance = modelRoot2.c // getParent const modelParent1 = getParent(modelInstance.c) const modelCP1: Instance = modelParent1 const modelParent2 = getParent>(modelInstance.c) const modelCP2: Instance = modelParent2 // array as root const ArrayOfC = types.array(C) const arrayInstance = ArrayOfC.create([C.create()]) // getRoot const arrayRoot1 = getRoot(arrayInstance[0]) const arrayCR1: Instance = arrayRoot1[0] // getParent const arrayParent1 = getParent(arrayInstance[0]) const arrayCP1: Instance = arrayParent1 // map as root const MapOfC = types.map(C) const mapInstance = MapOfC.create({ a: C.create() }) // getRoot const mapRoot1 = getRoot(mapInstance.get("a")!) const mapC1: Instance = mapRoot1.get("a")! // getParent const mapParent1 = getRoot(mapInstance.get("a")!) const mapCP1: Instance = mapParent1 }) test("cast and SnapshotOrInstance", () => { const NumberArray = types.array(types.number) const NumberMap = types.map(types.number) const A = types .model({ n: 123, n2: types.number, arr: NumberArray, map: NumberMap }) .actions(self => ({ // for primitives (although not needed) setN(nn: SnapshotOrInstance) { self.n = cast(nn) }, setN2(nn: SnapshotOrInstance) { self.n = cast(nn) }, setN3(nn: SnapshotOrInstance) { self.n = cast(nn) }, setN4(nn: number) { self.n = cast(nn) }, setN5() { self.n = cast(5) }, // for arrays setArr(nn: SnapshotOrInstance) { self.arr = cast(nn) }, setArr2(nn: SnapshotOrInstance) { self.arr = cast(nn) }, setArr3(nn: SnapshotIn) { self.arr = cast(nn) }, setArr31(nn: number[]) { self.arr = cast(nn) }, setArr4() { // it works even without specifying the target type, magic! self.arr = cast([2, 3, 4]) self.arr = cast(NumberArray.create([2, 3, 4])) }, // for maps setMap(nn: SnapshotOrInstance) { self.map = cast(nn) }, setMap2(nn: SnapshotOrInstance) { self.map = cast(nn) }, setMap3(nn: SnapshotIn) { self.map = cast(nn) }, setMap31(nn: { [k: string]: number }) { self.map = cast(nn) }, setMap4() { // it works even without specifying the target type, magic! self.map = cast({ a: 2, b: 3 }) self.map = cast(NumberMap.create({ a: 2, b: 3 })) } })) const C = types .model({ a: A, maybeA: types.maybe(A), maybeNullA: types.maybeNull(A) }) .actions(self => ({ // for submodels, using typeof self.var setA(na: SnapshotOrInstance) { self.a = cast(na) // we just want to check it compiles if (0 !== 0) { self.maybeA = cast(na) self.maybeNullA = cast(na) } }, // for submodels, using the type directly setA2(na: SnapshotOrInstance) { self.a = cast(na) // we just want to check it compiles if (0 !== 0) { self.maybeA = cast(na) self.maybeNullA = cast(na) } }, setA3(na: SnapshotIn) { self.a = cast(na) // we just want to check it compiles if (0 !== 0) { self.maybeA = cast(na) self.maybeNullA = cast(na) } }, setA4(na: Instance) { self.a = cast(na) // we just want to check it compiles if (0 !== 0) { self.maybeA = cast(na) self.maybeNullA = cast(na) } }, setA5() { // it works even without specifying the target type, magic! self.a = cast({ n2: 5 }) self.a = cast(A.create({ n2: 5 })) // we just want to check it compiles if (0 !== 0) { self.maybeA = cast({ n2: 5 }) self.maybeA = cast(A.create({ n2: 5 })) self.maybeNullA = cast({ n2: 5 }) self.maybeNullA = cast(A.create({ n2: 5 })) } } })) const c = C.create({ a: { n2: 5 } }) unprotect(c) // all below works c.setA({ n2: 5 }) c.setA(A.create({ n2: 5 })) c.setA2({ n2: 5 }) c.setA2(A.create({ n2: 5 })) c.setA3({ n2: 5 }) // c.setA3(A.create({ n2: 5 })) // this one doesn't work (as expected, it wants the creation type) // c.setA4({n2: 5}) // this one doesn't work (as expected, it wants the instance type) c.setA4(A.create({ n2: 5 })) c.setA5() c.a.setN(1) c.a.setN2(1) c.a.setN3(1) c.a.setN4(1) c.a.setN5() c.a.setArr([]) c.a.setArr(NumberArray.create([])) c.a.setArr2([]) c.a.setArr2(NumberArray.create([])) c.a.setArr3([]) c.a.setArr3(NumberArray.create([])) c.a.setArr4() c.a.setMap({ a: 2, b: 3 }) c.a.setMap(NumberMap.create({ a: 2, b: 3 })) c.a.setMap2({ a: 2, b: 3 }) c.a.setMap2(NumberMap.create({ a: 2, b: 3 })) c.a.setMap3({ a: 2, b: 3 }) // c.a.setMap3(NumberMap.create({ a: 2, b: 3 })) // doesn't work (as expected, wants a plain object) c.a.setMap4() const arr = types.array(A).create() unprotect(arr) arr[0] = cast({ n2: 5 }) const map = types.map(A).create() unprotect(map) map.set("a", cast({ n2: 5 })) // not really needed in this case, but whatever :) // this does not compile, yay! /* cast([]) cast({ a: 5 }) cast(NumberArray.create([])) cast(A.create({ n2: 5 })) cast({ a: 2, b: 5 }) cast(NumberMap.create({ a: 2, b: 3 })) */ }) test("#994", () => { const Cinema = types.model("Cinema", { id: types.identifier, name: types.maybe(types.string) }) const ref = types.reference(Cinema) // should compile ok on TS3 }) test("castToSnapshot", () => { const firstModel = types.model({ brew1: types.map(types.number) }) const secondModel = types.model({ brew2: types.map(firstModel) }).actions(self => ({ do() {} })) const appMod = types.model({ aaa: secondModel }) const storeSnapshot: SnapshotIn = { brew2: { outside: { brew1: { inner: 222 } } } } const storeInstance = secondModel.create(storeSnapshot) const storeSnapshotOrInstance1: SnapshotOrInstance = secondModel.create(storeSnapshot) const storeSnapshotOrInstance2: SnapshotOrInstance = storeSnapshot appMod.create({ aaa: castToSnapshot(storeInstance) }) appMod.create({ aaa: castToSnapshot(storeSnapshot) }) appMod.create({ aaa: castToSnapshot(storeSnapshotOrInstance1) }) appMod.create({ aaa: castToSnapshot(storeSnapshotOrInstance2) }) // appMod.create({ aaa: castToSnapshot(5) }) // should not compile }) test("create correctly chooses if the snapshot is needed or not - #920", () => { const X = types.model({ test: types.string }) const T = types.model({ test: types.refinement(X, s => s.test.length > 5) }) // T.create() // manual test: expects compilation error // T.create({}) // manual test: expects compilation error T.create({ test: { test: "hellothere" } }) const T2 = types.model({ test: types.maybe(X) }) T2.create() // ok T2.create({}) // ok const A = types.model({ test: "bla" }) A.create() // ok A.create({}) // ok const B = types.array(types.string) B.create() // ok B.create(["hi"]) // ok const C = types.map(types.string) C.create() // ok C.create({ hi: "hi" }) // ok const D = types.number // D.create() // manual test: expects compilation error D.create(5) // ok const E = types.optional(types.number, 5) E.create() // ok E.create(6) // ok const F = types.frozen() // F.create() // manual test: compilation error F.create(6) // ok const FF = types.frozen() FF.create() // ok FF.create(undefined) // ok const G = types.frozen(5) G.create() // ok G.create(6) // ok const H = types.frozen(5) H.create() // ok H.create(6) // ok const I = types.optional(types.frozen(), 6) I.create() I.create(7) }) test("#1117", () => { const Failsafe = ( t: IType, handleProblem: ( value: C, validationError: ReturnType["validate"]> ) => void = (value, error) => { console.error("Skipping value: typecheck error on", value) console.error(error) } ) => types.custom({ name: `Failsafe<${t.name}>`, fromSnapshot(snapshot: C) { try { return t.create(snapshot) // this should compile } catch (e) { handleProblem(snapshot, e as any) return null } }, toSnapshot(x) { if (isStateTreeNode(x)) return getSnapshot(x) return x as any as C }, isTargetType(v): v is T | null { if (isFrozenType(t)) { return t.is(v) } return false }, getValidationMessage() { return "" } }) }) test("MST array type should be assignable to plain array type", () => { { const Todo = types .model({ done: false, name: types.string }) .actions(self => ({ toggleDone() { self.done = !self.done } })) const TodoArray = types.array(Todo) const todoArray = TodoArray.create([{ done: true, name: "todo1" }, { name: "todo2" }]) unprotect(todoArray) const otherTodoArray: Array> = todoArray otherTodoArray.push(cast({ done: false, name: "todo2" })) } { const T = types.model({ a: types.optional(types.array(types.number), []) }) const arr: Array = T.create().a } { const T = types.model({ a: types.optional(types.array(types.number), [], [5]) }) const arr: Array = T.create({ a: 5 }).a } }) test("can get snapshot from submodel (submodel is IStateNodeTree", () => { const T = types.model({ a: types.model({ x: 5 }) }) const t = T.create({ a: {} }) const sn = getSnapshot(t.a).x }) test("can extract type from complex objects", () => { const T = types.maybe( types.model({ a: types.model({ x: 5 }) }) ) const t = T.create({ a: {} })! type OriginalType = TypeOfValue const T2: OriginalType = T }) test("#1268", () => { const Book = types.model({ id: types.identifier }) const BooksStore = types.model({ books: types.array(types.reference(Book)) }) const RootStore = types.model({ booksStore: BooksStore }) const booksStore = BooksStore.create({ books: [] }) const rootStore = RootStore.create({ booksStore: castToSnapshot(booksStore) }) }) test("#1307 optional can be omitted in .create", () => { const Model1 = types.model({ name: types.optional(types.string, "") }) const model1 = Model1.create({}) assertTypesEqual(model1.name, _ as string) const Model2 = types.model({ name: "" }) const model2 = Model2.create({}) assertTypesEqual(model2.name, _ as string) }) test("#1307 custom types failing", () => { const createCustomType = ({ CustomType }: { CustomType: ICustomType }) => { return types .model("Example", { someProp: types.boolean, someType: CustomType }) .views(self => ({ get isSomePropTrue(): boolean { return self.someProp } })) } }) test("#1343", () => { function createTypeA(t: T) { return types.model("TypeA", t).views(self => ({ get someView() { return null } })) } function createTypeB(t: T) { return types .model("TypeB", { a: createTypeA(t) }) .views(self => ({ get someViewFromA() { return self.a.someView } })) } }) test("#1330", () => { const ChildStore = types .model("ChildStore", { foo: types.string, bar: types.boolean }) .views(self => ({ get root(): IRootStore { return getRoot(self) } })) .actions(self => ({ test() { const { childStore } = self.root // childStore and childStore.foo is properly inferred in TS 3.4 but not in 3.5 console.log(childStore.foo) } })) interface IRootStore extends Instance {} const RootStore = types.model("RootStore", { childStore: ChildStore, test: "" }) assertTypesEqual( RootStore.create({ childStore: { foo: "a", bar: true } }).childStore.root.test, _ as string ) }) test("maybe / optional type inference verification", () => { const T = types.model({ a: types.string, b: "test", c: types.maybe(types.string), d: types.maybeNull(types.string), e: types.optional(types.string, "test") }) interface ITC extends SnapshotIn {} interface ITS extends SnapshotOut {} assertTypesEqual( _ as ITC, _ as { a: string b?: string c?: string | undefined d?: string | null e?: string } ) assertTypesEqual( _ as ITS, _ as { a: string b: string c: string | undefined d: string | null e: string } ) }) test("object creation with no props", () => { const MyType = types.model().views(_self => ({ get test() { return 5 } })) MyType.create() MyType.create({}) // Instances can be created with their own instance type MyType.create(MyType.create()) // Instances can be created with a snapshot of themselves true || MyType.create(getSnapshot(MyType.create())) // TODO @ts-expect-error -- symbols aren't props (but may be one day) // This currently is allowed, because excess property checking doesn't happen against symbols. // See https://github.com/microsoft/TypeScript/issues/44794 true || MyType.create({ [Symbol("test")]: 5 }) // @ts-expect-error -- this is a view, not a prop true || MyType.create({ test: 5 }) // @ts-expect-error -- unknown prop true || MyType.create({ another: 5 }) }) test("object creation when composing with a model with no props", () => { const EmptyType = types.model().views(_self => ({ get test() { return 5 } })) const NonEmptyType = types.model({ value: types.optional(types.number, 0), negate: types.boolean }) const Composed = types.compose(EmptyType, NonEmptyType) Composed.create({ negate: true }) Composed.create({ negate: false }) Composed.create({ value: 5, negate: true }) // Instances can be created with their own instance type Composed.create(Composed.create({ negate: true })) // @ts-expect-error -- symbols aren't props (but may be one day) true || Composed.create({ [Symbol("test")]: 5 }) // @ts-expect-error -- this is a view, not a prop true || Composed.create({ test: 5 }) // @ts-expect-error -- unknown prop true || Composed.create({ another: 5 }) }) test("union type inference verification for small number of types", () => { const T = types.union(types.boolean, types.literal("test"), types.maybe(types.number)) type ITC = SnapshotIn type ITS = SnapshotOut assertTypesEqual(_ as ITC, _ as boolean | "test" | number | undefined) assertTypesEqual(_ as ITS, _ as boolean | "test" | number | undefined) }) test("union type inference verification for a large number of types", () => { const T = types.union( types.literal("a"), types.literal("b"), types.literal("c"), types.literal("d"), types.literal("e"), types.literal("f"), types.literal("g"), types.literal("h"), types.literal("i"), types.literal("j") ) type ITC = SnapshotIn type ITS = SnapshotOut assertTypesEqual(_ as ITC, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j") assertTypesEqual(_ as ITS, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j") }) test("#2186 substitutability type verification for model types extending a common base", () => { const BaseType = types.model() const SubTypeOptional = BaseType.props({ a: "" }) const SubTypeRequired = BaseType.props({ a: types.string }) const SubTypeRequiredWithAnotherOptional = SubTypeRequired.props({ a: types.string, b: 5 }) true || ((t: typeof BaseType) => t.create())(SubTypeOptional) true || ((t: typeof SubTypeRequired) => t.create({ a: "123" }))(SubTypeRequiredWithAnotherOptional) true || ((t: typeof BaseType) => t.create())( // @ts-expect-error -- a is required SubTypeRequired ) }) test("#2184 - type narrowing functions should narrow to the expected type", () => { const type: unknown = null if (isOptionalType(type)) { assertTypesEqual(type, _ as IOptionalIType) } else if (isUnionType(type)) { assertTypesEqual(type, _ as IUnionType) } else if (isFrozenType(type)) { assertTypesEqual(type, _ as ISimpleType) } else if (isMapType(type)) { assertTypesEqual(type, _ as IMapType) } else if (isArrayType(type)) { assertTypesEqual(type, _ as IArrayType) } else if (isModelType(type)) { assertTypesEqual(type, _ as IAnyModelType) } else if (isLiteralType(type)) { assertTypesEqual(type, _ as ISimpleType) } else if (isPrimitiveType(type)) { assertTypesEqual( type, _ as | ISimpleType | ISimpleType | ISimpleType | typeof types.bigint | typeof DatePrimitive ) } else if (isReferenceType(type)) { assertTypesEqual(type, _ as IReferenceType) } else if (isIdentifierType(type)) { assertTypesEqual(type, _ as ISimpleType | ISimpleType) } else if (isRefinementType(type)) { assertTypesEqual(type, _ as IAnyType) } else if (isLateType(type)) { assertTypesEqual(type, _ as IAnyType) } }) describe("for snapshotProcessor", () => { const Model = types.model({ name: types.optional(types.string, "string") }) test("produces the right types when not customized", () => { const Processor = types.snapshotProcessor(Model, { preProcessor(snapshot) { assertTypesEqual(snapshot, _ as SnapshotIn) return snapshot }, postProcessor(snapshot) { assertTypesEqual(snapshot, _ as { name: string }) return snapshot } }) type ITC = SnapshotIn type ITS = SnapshotOut assertTypesEqual(_ as ITC, _ as SnapshotIn) assertTypesEqual(_ as ITS, _ as SnapshotOut) }) test("produces the right types when customized", () => { const Processor = types.snapshotProcessor(Model, { preProcessor(snapshot) { assertTypesEqual(snapshot, _ as string) return Model.create({ name: snapshot }) }, postProcessor(snapshot) { assertTypesEqual(snapshot, _ as SnapshotOut) return snapshot.name.length } }) type ITC = SnapshotIn type ITS = SnapshotOut assertTypesEqual(_ as ITC, _ as string) assertTypesEqual(_ as ITS, _ as number) }) }) test("#1627 - union dispatch function is typed", () => { const model = types.model({ a: types.string }) const _union = types.union( { dispatcher(snapshot) { assertTypesEqual( snapshot, _ as SnapshotIn | SnapshotIn ) return snapshot?.a ? model : types.null } }, model, types.null ) const _brokenUnion = types.union( { // @ts-expect-error -- types.string isn't a valid type for the union dispatcher(snapshot) { assertTypesEqual( snapshot, _ as SnapshotIn | SnapshotIn ) return snapshot?.a ? model : types.string } }, model, types.null ) }) ================================================ FILE: __tests__/core/union.test.ts ================================================ import { configure } from "mobx" import { types, hasParent, tryResolve, getSnapshot, applySnapshot, getType, setLivelinessChecking, SnapshotIn, Instance, IAnyType } from "../../src" import { describe, expect, it, test, beforeEach } from "bun:test" const createTestFactories = () => { const Box = types.model("Box", { width: types.number, height: types.number }) const Square = types.model("Square", { width: types.number }) const Cube = types.model("Cube", { width: types.number, height: types.number, depth: types.number }) const Plane = types.union(Square, Box) const Heighed = types.union(Box, Cube) const DispatchPlane = types.union( { dispatcher: snapshot => (snapshot && "height" in snapshot ? Box : Square) }, Box, Square ) const Block = types.model("Block", { list: types.array(Heighed) }) return { Box, Square, Cube, Plane, DispatchPlane, Heighed, Block } } const createLiteralTestFactories = () => { const Man = types.model("Man", { type: types.literal("M") }) const Woman = types.model("Woman", { type: types.literal("W") }) const All = types.model("All", { type: types.string }) const ManWomanOrAll = types.union(Man, Woman, All) return { Man, Woman, All, ManWomanOrAll } } if (process.env.NODE_ENV !== "production") { test("it should complain about multiple applicable types no dispatch method", () => { const { Box, Square } = createTestFactories() const PlaneNotEager = types.union({ eager: false }, Square, Box) expect(() => { PlaneNotEager.create({ width: 2, height: 2 }) }).toThrow(/Error while converting/) }) } test("it should have parent whenever creating or applying from a complex data structure to a model which has Union typed children", () => { const { Block, Heighed } = createTestFactories() const block = Block.create({ list: [{ width: 2, height: 2 }] }) const child = tryResolve(block, "./list/0") expect(hasParent(child)).toBe(true) }) if (process.env.NODE_ENV !== "production") { test("it should complain about no applicable types", () => { const { Heighed } = createTestFactories() expect(() => { Heighed.create({ height: 2 } as any) }).toThrow(/Error while converting/) }) } test("it should be smart enough to discriminate by keys", () => { const { Box, Plane, Square } = createTestFactories() const doc = types.union(Square, Box).create({ width: 2 }) expect(Box.is(doc)).toEqual(false) expect(Square.is(doc)).toEqual(true) }) test("it should discriminate by value type", () => { const Size = types.model("Size", { width: 0, height: 0 }) const Picture = types.model("Picture", { url: "", size: Size }) const Square = types.model("Square", { size: 0 }) const PictureOrSquare = types.union(Picture, Square) const doc = PictureOrSquare.create({ size: { width: 0, height: 0 } }) expect(Picture.is(doc)).toEqual(true) expect(Square.is(doc)).toEqual(false) }) test("it should compute exact union types", () => { const { Box, Plane, Square } = createTestFactories() expect(Plane.is(Box.create({ width: 3, height: 2 }))).toEqual(true) expect(Plane.is(Square.create({ width: 3 }))).toEqual(true) }) test("it should compute exact union types - 2", () => { const { Box, DispatchPlane, Square } = createTestFactories() expect(DispatchPlane.is(Box.create({ width: 3, height: 2 }))).toEqual(true) expect( DispatchPlane.is( Square.create({ width: 3, height: 2 } as any /* incorrect type, superfluous attr!*/) ) ).toEqual(true) }) test("it should use dispatch to discriminate", () => { const { Box, DispatchPlane, Square } = createTestFactories() const a = DispatchPlane.create({ width: 3 }) expect(getSnapshot(a)).toEqual({ width: 3 }) }) test("it should eagerly match by ambiguos value", () => { const { ManWomanOrAll, All, Man } = createLiteralTestFactories() const person = ManWomanOrAll.create({ type: "Z" }) expect(All.is(person)).toEqual(true) expect(Man.is(person)).toEqual(false) }) test("it should eagerly match by ambiguos value - 2", () => { const { All, Man } = createLiteralTestFactories() const person = types.union(All, Man).create({ type: "M" }) expect(All.is(person)).toEqual(true) expect(Man.is(person)).toEqual(false) // not matched, All grabbed everything! }) test("it should eagerly match by value literal", () => { const { ManWomanOrAll, All, Man } = createLiteralTestFactories() const person = ManWomanOrAll.create({ type: "M" }) expect(All.is(person)).toEqual(false) expect(Man.is(person)).toEqual(true) }) test("dispatch", () => { const Odd = types .model({ value: types.number }) .actions(self => ({ isOdd() { return true }, isEven() { return false } })) const Even = types.model({ value: types.number }).actions(self => ({ isOdd() { return false }, isEven() { return true } })) const Num = types.union( { dispatcher: snapshot => (snapshot.value % 2 === 0 ? Even : Odd) }, Even, Odd ) expect(Num.create({ value: 3 }).isOdd()).toBe(true) expect(Num.create({ value: 3 }).isEven()).toBe(false) expect(Num.create({ value: 4 }).isOdd()).toBe(false) expect(Num.create({ value: 4 }).isEven()).toBe(true) if (process.env.NODE_ENV !== "production") { expect(() => { types.union( ((snapshot: any) => (snapshot.value % 2 === 0 ? Even : Odd)) as any, // { dispatcher: snapshot => (snapshot.value % 2 === 0 ? Even : Odd) }, Even, Odd ) }).toThrow("expected object") } }) test("961 - apply snapshot to union should not throw when union keeps models with different properties and snapshot is got by getSnapshot", () => { const Foo = types.model({ foo: 1 }) const Bar = types.model({ bar: 1 }) const U = types.union(Foo, Bar) const u = U.create({ foo: 1 }) applySnapshot(u, getSnapshot(Bar.create())) }) describe("1045 - secondary union types with applySnapshot and ids", () => { function initTest( useSnapshot: boolean, useCreate: boolean, submodel1First: boolean, type: number ) { setLivelinessChecking("error") const Submodel1NoSP = types.model("Submodel1", { id: types.identifier, extraField1: types.string, extraField2: types.maybe(types.string) }) const Submodel1SP = types.snapshotProcessor(Submodel1NoSP, { preProcessor(sn: SnapshotIn>) { const { id, extraField1, extraField2 } = sn return { id, extraField1: extraField1.toUpperCase(), extraField2: extraField2?.toUpperCase() } } }) const Submodel2NoSP = types.model("Submodel2", { id: types.identifier, extraField1: types.maybe(types.string), extraField2: types.string }) const Submodel2SP = types.snapshotProcessor(Submodel2NoSP, { preProcessor(sn: SnapshotIn>) { const { id, extraField1, extraField2 } = sn return { id, extraField1: extraField1?.toUpperCase(), extraField2: extraField2.toUpperCase() } } }) const Submodel1 = useSnapshot ? Submodel1SP : Submodel1NoSP const Submodel2 = useSnapshot ? Submodel2SP : Submodel2NoSP const Submodel = submodel1First ? types.union(Submodel1, Submodel2) : types.union(Submodel2, Submodel1) const Model = types.array(Submodel) const store = Model.create([{ id: "id1", extraField1: "extraField1" }]) return { store, applySn: function () { const sn1 = { id: "id1", extraField1: "new extraField1", extraField2: "some value" } const sn2 = { id: "id1", extraField1: undefined, extraField2: "some value" } const sn = type === 1 ? sn1 : sn2 const submodel = type === 1 ? Submodel1 : Submodel2 const expected = useSnapshot ? { id: sn.id, extraField1: sn.extraField1?.toUpperCase(), extraField2: sn.extraField2?.toUpperCase() } : sn applySnapshot(store, [useCreate ? (submodel as any).create(sn) : sn]) expect(store.length).toBe(1) expect(store[0]).toEqual(expected) expect(getType(store[0])).toBe( useSnapshot ? (submodel.getSubTypes() as IAnyType) : submodel ) } } } for (const useSnapshot of [false, true]) { describe(useSnapshot ? "with snapshotProcessor" : "without snapshotProcessor", () => { for (const submodel1First of [true, false]) { describe(submodel1First ? "submodel1 first" : "submodel2 first", () => { for (const useCreate of [false, true]) { describe(useCreate ? "using create" : "not using create", () => { for (const type of [2, 1]) { describe(`snapshot is of type Submodel${type}`, () => { beforeEach(() => { configure({ useProxies: "never" }) }) it(`apply snapshot works when the node is not touched`, () => { const t = initTest( useSnapshot, useCreate, submodel1First, type ) t.applySn() }) it(`apply snapshot works when the node is touched`, () => { const t = initTest( useSnapshot, useCreate, submodel1First, type ) // tslint:disable-next-line:no-unused-expression t.store[0] t.applySn() }) }) } }) } }) } }) } }) ================================================ FILE: __tests__/core/volatile.test.ts ================================================ import { types, getSnapshot, recordPatches, unprotect } from "../../src" import { reaction, isObservableProp, isObservable, autorun, observable } from "mobx" import { expect, test } from "bun:test" const Todo = types .model({ done: false }) .volatile(self => ({ state: Promise.resolve(1) })) .actions(self => ({ toggle() { self.done = !self.done }, reload() { self.state = Promise.resolve(2) } })) test("Properties should be readable and writable", () => { const i = Todo.create() expect(i.state instanceof Promise).toBe(true) i.reload() expect(i.state instanceof Promise).toBe(true) }) test("VS should not show up in snapshots", () => { expect(getSnapshot(Todo.create())).toEqual({ done: false }) }) test("VS should not show up in patches", () => { const i = Todo.create() const r = recordPatches(i) i.reload() i.toggle() r.stop() expect(r.patches).toEqual([{ op: "replace", path: "/done", value: true }]) }) test("VS be observable", () => { const promises: Promise[] = [] const i = Todo.create() const d = reaction( () => i.state, p => promises.push(p) ) i.reload() i.reload() expect(promises.length).toBe(2) d() }) test("VS should not be deeply observable", () => { const i = types .model({}) .volatile(self => ({ x: { a: 1 } })) .create() unprotect(i) expect(isObservableProp(i, "x")).toBe(true) expect(isObservable(i.x)).toBe(false) expect(isObservableProp(i.x, "a")).toBe(false) i.x = { a: 2 } expect(isObservableProp(i, "x")).toBe(true) expect(isObservable(i.x)).toBe(false) expect(isObservableProp(i.x, "a")).toBe(false) }) test("VS should not be strongly typed observable", () => { const i = Todo.create() // TEST: type error i.state = 7 i.state.then(() => {}) // it's a promise // TEST: not available on snapshot: getSnapshot(i).state expect(true).toBe(true) }) test("VS should not be modifiable without action", () => { const i = Todo.create() expect(() => { i.state = Promise.resolve(4) }).toThrow(/the object is protected and can only be modified by using an action/) }) test("VS should expect a function as an argument", () => { expect(() => { const t = types .model({}) // @ts-ignore .volatile({ state: 1 }) .create() }).toThrow(`You passed an object to volatile state as an argument, when function is expected`) }) test("VS should not be modifiable when unprotected", () => { const i = Todo.create() unprotect(i) const p = Promise.resolve(7) expect(() => { i.state = p }).not.toThrow() expect(i.state === p).toBe(true) }) test("VS sample from the docs should work (1)", () => { const T = types.model({}).extend(self => { const localState = observable.box(3) return { views: { get x() { return localState.get() } }, actions: { setX(value: number) { localState.set(value) } } } }) const t = T.create() expect(t.x).toBe(3) t.setX(5) expect(t.x).toBe(5) // now observe it const observed: number[] = [] const dispose = autorun(() => { observed.push(t.x) }) t.setX(7) expect(t.x).toBe(7) expect(observed).toEqual([5, 7]) dispose() }) test("VS sample from the docs should work (2)", () => { const T = types.model({}).extend(self => { let localState = 3 return { views: { getX() { return localState } }, actions: { setX(value: number) { localState = value } } } }) const t = T.create() expect(t.getX()).toBe(3) t.setX(5) expect(t.getX()).toBe(5) // now observe it (should not be observable) const observed: number[] = [] const dispose = autorun(() => { observed.push(t.getX()) }) t.setX(7) expect(t.getX()).toBe(7) expect(observed).toEqual([5]) dispose() }) ================================================ FILE: __tests__/perf/fixture-data.skip.ts ================================================ import { rando, createHeros, createMonsters, createTreasure } from "./fixtures/fixture-data" import { Hero, Monster, Treasure } from "./fixtures/fixture-models" import { expect, test } from "bun:test" test("createHeros", () => { const data = createHeros(10) expect(data.length).toBe(10) const hero = Hero.create(data[0]) expect(hero.descriptionLength > 1).toBe(true) }) test("createMonsters", () => { const data = createMonsters(10, 10, 10) expect(data.length).toBe(10) expect(data[1].treasures.length).toBe(10) expect(data[0].eatenHeroes.length).toBe(10) const monster = Monster.create(data[0]) expect(monster.eatenHeroes && monster.eatenHeroes.length === 10).toBe(true) expect(monster.treasures.length === 10).toBe(true) }) test("createTreasure", () => { const data = createTreasure(10) expect(data.length).toBe(10) const treasure = Treasure.create(data[1]) expect(treasure.gold > 0).toBe(true) }) test("rando sorting", () => { // i'm going straight to hell for this test... must get coverage to 100%.... no matter the cost. let foundTrue = false let foundFalse = false let result do { result = rando() if (result) { foundTrue = true } else { foundFalse = true } } while (!foundTrue || !foundFalse) expect(foundTrue).toBe(true) expect(foundFalse).toBe(true) }) ================================================ FILE: __tests__/perf/fixture-models.skip.ts ================================================ import { Hero, Monster, Treasure } from "./fixtures/fixture-models" import { expect, test } from "bun:test" const mst = require("../../dist/mobx-state-tree.umd") const { unprotect } = mst const SAMPLE_HERO = { id: 1, name: "jimmy", level: 1, role: "cleric", description: "hi" } test("Hero computed fields", () => { const hero = Hero.create(SAMPLE_HERO) expect(hero.descriptionLength).toBe(2) }) test("Tresure", () => { const treasure = Treasure.create({ gold: 1, trapped: true }) expect(treasure.trapped).toBe(true) expect(treasure.gold).toBe(1) }) test("Monster computed fields", () => { const monster = Monster.create({ id: "foo", level: 1, maxHp: 3, hp: 1, warning: "boo!", createdAt: new Date(), treasures: [ { gold: 2, trapped: true }, { gold: 3, trapped: true } ], eatenHeroes: [SAMPLE_HERO], hasFangs: true, hasClaws: true, hasWings: true, hasGrowl: true, freestyle: null }) expect(monster.isAlive).toBe(true) expect(monster.isFlashingRed).toBe(true) unprotect(monster) expect(monster.weight).toBe(2) monster.level = 0 monster.hasFangs = false monster.hasWings = false monster.eatenHeroes = null expect(monster.weight).toBe(1) monster.hp = 0 expect(monster.isFlashingRed).toBe(false) expect(monster.isAlive).toBe(false) }) ================================================ FILE: __tests__/perf/fixtures/fixture-data.ts ================================================ import { HeroRoles } from "./fixture-models" /** * Creates data containing very few fields. * * @param count The number of items to create. */ export function createTreasure(count: number) { const data = [] let i = 0 do { data.push({ trapped: i % 2 === 0, gold: ((count % 10) + 1) * 10 }) i++ } while (i < count) return data } // why yes i DID graduate high school, why do you ask? export const rando = () => (Math.random() > 0.5 ? 1 : 0) const titles = ["Sir", "Lady", "Baron von", "Baroness", "Captain", "Dread", "Fancy"].sort(rando) const givenNames = ["Abe", "Beth", "Chuck", "Dora", "Ernie", "Fran", "Gary", "Haily"].sort(rando) const epicNames = ["Amazing", "Brauny", "Chafed", "Dapper", "Egomaniac", "Foul"].sort(rando) const wtf = `Daenerys Stormborn of the House Targaryen, First of Her Name, the Unburnt, Queen of the Andals and the First Men, Khaleesi of the Great Grass Sea, Breaker of Chains, and Mother of Dragons. ` /** * Creates data with a medium number of fields and data. * * @param count The number of items to create. */ export function createHeros(count: number) { const data = [] let i = 0 let even = true let n1 let n2 let n3 do { n1 = titles[i % titles.length] n2 = givenNames[i % givenNames.length] n3 = epicNames[i % epicNames.length] data.push({ id: i, name: `${n1} ${n2} the ${n3}`, level: (count % 100) + 1, role: HeroRoles[i % HeroRoles.length], description: `${wtf} ${wtf} ${wtf}` }) even = !even i++ } while (i < count) return data } /** * Creates data with a large number of fields and data. * * @param count The number of items to create. * @param treasureCount The number of small children to create. * @param heroCount The number of medium children to create. */ export function createMonsters(count: number, treasureCount: number, heroCount: number) { const data = [] let i = 0 let even = true do { const treasures = createTreasure(treasureCount) const eatenHeroes = createHeros(heroCount) data.push({ id: `omg-${i}-run!`, freestyle: `${wtf} ${wtf} ${wtf}${wtf} ${wtf} ${wtf}`, level: (count % 100) + 1, hp: i % 2 === 0 ? 1 : 5 * i, maxHp: 5 * i, warning: "!!!!!!", createdAt: new Date(), hasFangs: even, hasClaws: even, hasWings: !even, hasGrowl: !even, fearsFire: even, fearsWater: !even, fearsWarriors: even, fearsClerics: !even, fearsMages: even, fearsThieves: !even, stenchLevel: i % 5, treasures, eatenHeroes }) even = !even i++ } while (i < count) return data } ================================================ FILE: __tests__/perf/fixtures/fixture-models.ts ================================================ const mst = require("../../../dist/mobx-state-tree.umd") const { types } = mst // tiny export const Treasure = types.model("Treasure", { trapped: types.boolean, gold: types.optional(types.number, 0) }) // medium export const HeroRoles = ["warrior", "wizard", "cleric", "thief"] export const Hero = types .model("Hero", { id: types.identifierNumber, name: types.string, description: types.string, level: types.optional(types.number, 1), role: types.union(...exports.HeroRoles.map(types.literal)) }) .views((self: any) => ({ get descriptionLength() { return self.description.length } })) // large export const Monster = types .model("Monster", { id: types.identifier, freestyle: types.frozen(), level: types.number, maxHp: types.number, hp: types.number, warning: types.maybeNull(types.string), createdAt: types.maybeNull(types.Date), treasures: types.optional(types.array(exports.Treasure), []), eatenHeroes: types.maybeNull(types.array(exports.Hero)), hasFangs: types.optional(types.boolean, false), hasClaws: types.optional(types.boolean, false), hasWings: types.optional(types.boolean, false), hasGrowl: types.optional(types.boolean, false), stenchLevel: types.optional(types.number, 0), fearsFire: types.optional(types.boolean, false), fearsWater: types.optional(types.boolean, false), fearsWarriors: types.optional(types.boolean, false), fearsClerics: types.optional(types.boolean, false), fearsMages: types.optional(types.boolean, false), fearsThieves: types.optional(types.boolean, false), fearsProgrammers: types.optional(types.boolean, true) }) .views((self: any) => ({ get isAlive() { return self.hp > 0 }, get isFlashingRed() { return self.hp > 0 && self.hp < self.maxHp && self.hp === 1 }, get weight() { const victimWeight = self.eatenHeroes ? self.eatenHeroes.length : 0 const fangWeight = self.hasFangs ? 10 : 5 const wingWeight = self.hasWings ? 12 : 4 return (victimWeight + fangWeight + wingWeight) * self.level > 5 ? 2 : 1 } })) ================================================ FILE: __tests__/perf/perf.skip.ts ================================================ import { smallScenario, mediumScenario, largeScenario } from "./scenarios" import { start } from "./timer" import { expect, test } from "bun:test" // TODO: Not sure how this should work. This feels super fragile. const TOO_SLOW_MS = 10000 test("performs well on small scenario", () => { expect(smallScenario(10).elapsed < TOO_SLOW_MS).toBe(true) }) test("performs well on medium scenario", () => { expect(mediumScenario(10).elapsed < TOO_SLOW_MS).toBe(true) }) test("performs well on large scenario", () => { expect(largeScenario(10, 0, 0).elapsed < TOO_SLOW_MS).toBe(true) expect(largeScenario(10, 10, 0).elapsed < TOO_SLOW_MS).toBe(true) expect(largeScenario(10, 0, 10).elapsed < TOO_SLOW_MS).toBe(true) expect(largeScenario(10, 10, 10).elapsed < TOO_SLOW_MS).toBe(true) }) test("timer", async () => { const go = start() await new Promise(resolve => setTimeout(resolve, 2)) const lap = go(true) await new Promise(resolve => setTimeout(resolve, 2)) const d = go() expect(lap).not.toBe(0) expect(d).not.toBe(0) expect(lap).not.toBe(d) }) ================================================ FILE: __tests__/perf/report.ts ================================================ import { smallScenario, mediumScenario, largeScenario } from "./scenarios" // here's what we'll be testing const plan = [ "-----------", "Small Model", "-----------", () => smallScenario(100), () => smallScenario(1000), () => smallScenario(10000), () => smallScenario(1000), () => smallScenario(100), "", "------------", "Medium Model", "------------", () => mediumScenario(100), () => mediumScenario(1000), () => mediumScenario(10000), () => mediumScenario(1000), () => mediumScenario(100), "", "------------------------", "Large Model - 0 children", "------------------------", () => largeScenario(100, 0, 0), () => largeScenario(1000, 0, 0), () => largeScenario(100, 0, 0), "", "-------------------------------------------", "Large Model - 10 small & 10 medium children", "-------------------------------------------", () => largeScenario(50, 10, 10), () => largeScenario(250, 10, 10), () => largeScenario(50, 10, 10), "", "-------------------------------------------", "Large Model - 100 small & 0 medium children", "-------------------------------------------", () => largeScenario(50, 100, 0), () => largeScenario(250, 100, 0), () => largeScenario(50, 100, 0), "", "-------------------------------------------", "Large Model - 0 small & 100 medium children", "-------------------------------------------", () => largeScenario(50, 0, 100), () => largeScenario(250, 0, 100), () => largeScenario(50, 0, 100) ] // burn a few to get the juices flowing smallScenario(1000) mediumScenario(500) largeScenario(100, 10, 10) // remember when this broke the internet? function leftPad(value: string, length: number, char = " "): string { return value.toString().length < length ? leftPad(char + value, length) : value } // let's start plan.forEach(fn => { // strings get printed, i guess. if (typeof fn === "string") { console.log(fn) return } // trigger awkward gc up front if we can if (global.gc) { global.gc() } // run the report const result = fn() // calculate some fields const seconds = leftPad((result.elapsed / 1.0).toLocaleString(), 8) const times = leftPad(`x ${result.count.toLocaleString()}`, 10) const avg = leftPad((result.elapsed / result.count).toFixed(1), 4) // print console.log(`${seconds}ms | ${times} | ${avg}ms avg`) }) console.log("") ================================================ FILE: __tests__/perf/scenarios.ts ================================================ import { start } from "./timer" import { Treasure, Hero, Monster } from "./fixtures/fixture-models" import { createTreasure, createHeros, createMonsters } from "./fixtures/fixture-data" /** * Covers models with a trivial number of fields. * * @param count The number of records to create. */ export function smallScenario(count: number) { const data = createTreasure(count) // ready? const time = start() const converted = data.map(d => Treasure.create(d)) // go const elapsed = time() const sanity = converted.length === count return { count, elapsed, sanity } } /** * Covers models with a moderate number of fields + 1 computed field. * * @param count The number of records to create. */ export function mediumScenario(count: number) { const data = createHeros(count) // ready? const time = start() const converted = data.map(d => Hero.create(d)) // go const elapsed = time() const sanity = converted.length === count return { count, elapsed, sanity } } /** * Covers models with a large number of fields. * * @param count The number of records to create. * @param smallChildren The number of small children contained within. * @param mediumChildren The number of medium children contained within. */ export function largeScenario(count: number, smallChildren: number, mediumChildren: number) { const data = createMonsters(count, smallChildren, mediumChildren) // ready? const time = start() const converted = data.map(d => Monster.create(d)) // go const elapsed = time() const sanity = converted.length === count return { count, elapsed, sanity } } ================================================ FILE: __tests__/perf/timer.ts ================================================ /** * Start a timer which return a function, which when called show the * number of milliseconds since it started. * * Passing true will give the current lap time. * * Example: * ```ts * const time = start() * // 1 second later * time() // 1.00 * // 1 more second later * time() // 2.00 * time(true) // 1.00 * ``` */ export const start = () => { const started = process.hrtime() let last: [number, number] = [started[0], started[1]] return (lapTime = false) => { const final = process.hrtime(lapTime ? last : started) return Math.round((final[0] * 1e9 + final[1]) / 1e6) } } ================================================ FILE: __tests__/setup.ts ================================================ import { beforeEach, afterEach, mock } from "bun:test" import { resetNextActionId, setLivelinessChecking } from "../src/internal" import { configure } from "mobx" beforeEach(() => { setLivelinessChecking("warn") resetNextActionId() }) afterEach(() => { mock.restore() // Some tests turn off proxy support, so ensure it's always turned back on configure({ useProxies: "always" }) }) ================================================ FILE: __tests__/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "rootDir": "../", "module": "commonjs", "lib": ["es2017", "es2020.bigint", "dom"], "target": "es6", "sourceMap": false, "noEmit": true }, "include": ["**/*.ts"] } ================================================ FILE: __tests__/utils.test.ts ================================================ import { MstError } from "../src/utils" import { describe, expect, test } from "bun:test" describe("MstError custom error class", () => { test("with default message", () => { const error = new MstError() expect(error.message).toBe("[mobx-state-tree] Illegal state") }) test("with custom message", () => { const customMessage = "custom error message" const error = new MstError(customMessage) expect(error.message).toBe(`[mobx-state-tree] ${customMessage}`) }) test("instance of MstError", () => { const error = new MstError() expect(error).toBeInstanceOf(MstError) expect(error).toBeInstanceOf(Error) }) }) ================================================ FILE: bunfig.toml ================================================ [test] root = "./__tests__" preload = ["./__tests__/setup.ts"] ================================================ FILE: changelog.md ================================================ The manually-updated changelog has been discontinued. For versions > 4.0.0, go here to view changes: [https://github.com/mobxjs/mobx-state-tree/releases](https://github.com/mobxjs/mobx-state-tree/releases) # 4.0.0 [BREAKING CHANGE] MST 4.0 requires MobX 6 # 3.17.3 Add onValidated support to safeReference [#1540 by @orlovcs](https://github.com/mobxjs/mobx-state-tree/pull/1540) # 3.17.2 Fix incorrect access to global `fail` symbol [#1549](https://github.com/mobxjs/mobx-state-tree/pull/1549) # 3.17.1 Re-release 3.17.0 # 3.17.0 - Added experimental helpers toGenerator and toGeneratorFunction. [#1543](https://github.com/mobxjs/mobx-state-tree/pull/1543) by [@fruitraccoon](https://github.com/fruitraccoon) # 3.16.0 - Added search field to the docs - Custom types can now receive environments as second argument of the `fromSnapshot` option. [#1410](https://github.com/mobxjs/mobx-state-tree/pull/1410) by [@k-g-a](https://github.com/k-g-a) - Added option `maxHistoryLength` to the `UndoManager`, implements [#1417](https://github.com/mobxjs/mobx-state-tree/issues/1417) through [#1426](https://github.com/mobxjs/mobx-state-tree/pull/1426) by [@tibotiber](https://github.com/tibotiber). - Improved TypeScript typings of `flow`, fixes [#1378](https://github.com/mobxjs/mobx-state-tree/pull/1378) through [#1409](https://github.com/mobxjs/mobx-state-tree/pull/1409) by [@nulladdict](https://github.com/nulladdict) - Fixed that calling `createObservableInstanceIfNeeded` would execute an action, even if the function immediately returned. (significant since the extraneous actions would pollute the mobx dev-tools on mere accesses, eg. by ComplexType.prototype.getValue) Fixes [#1421](https://github.com/mobxjs/mobx-state-tree/issues/1421) trough [#1422](https://github.com/mobxjs/mobx-state-tree/pull/1422) by [@Venryx](https://github.com/Venryx) - Fix issue where `snapshotProcessor.is` does not correctly handle model instances. Fixes [#1494](https://github.com/mobxjs/mobx-state-tree/issues/1494) through [#1495](https://github.com/mobxjs/mobx-state-tree/pull/1495) by [@KevinSjoberg](https://github.com/KevinSjoberg) - Make sure that MST no longer requires `setImmediate` to be present, but fallback to other solutions. [#1501](https://github.com/mobxjs/mobx-state-tree/pull/1501) by [@isaachinman](https://github.com/isaachinman) # 3.15.0 - Fix for flow typings. This means that now using flows requires at least TypeScript v3.6 and that `castFlowReturn` becomes deprecated. - Fix for empty models / models with all properties set to optional being able to take any value in TypeScript through [#1269](https://github.com/mobxjs/mobx-state-tree/pull/1269) by [@xaviergonz](https://github.com/xaviergonz). # 3.14.1 - Made it possible to force full run-time type-checking (for better error messages) in production builds by setting `ENABLE_TYPE_CHECK=true` as environment variable. Fixes [#1332](https://github.com/mobxjs/mobx-state-tree/pull/1332) through [#1337](https://github.com/mobxjs/mobx-state-tree/pull/1337) by [@OverseePublic](https://github.com/OverseePublic) - Fixed an issue where `Type.is` doesn't behave correctly for types that has snapshot processors. Fixes [#1321](https://github.com/mobxjs/mobx-state-tree/issues/1321) through [#1323](https://github.com/mobxjs/mobx-state-tree/pull/1323) by [@Tucker-Eric](https://github.com/Tucker-Eric) - Changed the implementation of the internal `STNValue` type, to fix TS 3.5.3 compatibility. If somebody notices regressions in the TypeScript integration, please report. Fixes [#1343](https://github.com/mobxjs/mobx-state-tree/issues/1343), [#1307](https://github.com/mobxjs/mobx-state-tree/issues/1307) - Added `acceptsUndefined` as option for `safeReference` so it is more suitable to be used inside arrays/maps, through [#1245](https://github.com/mobxjs/mobx-state-tree/pull/1245) by [@xaviergonz](https://github.com/xaviergonz). # 3.14.0 - Fixed a regression with `atomic` middleware with async flows [#1250](https://github.com/mobxjs/mobx-state-tree/issues/1250). - Added filter function to `recordActions` to filter out recording some actions. Also added `recording` and `resume` methods. - Added `getRunningActionContext()` to get the currently executing MST action context (if any). Also added the action context helper functions `isActionContextChildOf()` and `isActionContextThisOrChildOf`. - Reduced type nesting to avoid Typescript 3.4 errors about infinite types. Sadly due to this change `types.create` is no longer smart enough in TS to know if skipping the snapshot parameter is valid or not. Through [#1251](https://github.com/mobxjs/mobx-state-tree/pull/1251) by [@xaviergonz](https://github.com/xaviergonz). # 3.13.0 - Fixed `Instance` not giving the proper type in Typescript when the type included both objects and primitives. - Through PR [#1196](https://github.com/mobxjs/mobx-state-tree/pull/1196) by [@xaviergonz](https://github.com/xaviergonz) - Added `createActionTrackerMiddleware2`, a more easy to use version of the first one, which makes creating middlewares for both sync and async actions more universal. - Added an optional filter to `recordPatches` to be able to skip recording certain patches. - `atomic` now uses the new `createActionTrackerMiddleware2`. - `UndoManager` fixes and improvements: - Uses the new `createActionTrackerMiddleware2`. - Added `clearUndo` and `clearRedo` to only clear those. - Added `undoLevels` and `redoLevels` to know how many undo/redo actions are available. - Made undo manager actions atomic, so they won't actually do any partial changes if for some reason they fail. - Fix for `withoutUndo` so it will only skip recording what is inside, not the whole action - fixes [#1195](https://github.com/mobxjs/mobx-state-tree/issues/1195). # 3.12.2 - Added more output formats for the library (common-js minified version and umd minified version). Note that now the umd version will be the development version while the new umd.min version will be the production version. This change is to keep it in sync with the parent mobx package. Also the npm package is now leaner since it mistakenly included separatedly compiled js files and source maps. # 3.12.1 - Fixed a regression with `getEnv` sometimes not returning the proper environment. - Fixed an issue where `map.put` would not work with snapshots of types with an optional id [#1131](https://github.com/mobxjs/mobx-state-tree/issues/1131) through [#1226](https://github.com/mobxjs/mobx-state-tree/pull/1226) by [@xaviergonz](https://github.com/xaviergonz). # 3.12.0 - Added `TypeOfValue` to extract the type of a complex (non primitive) variable in Typescript. - Fixed some Typescript issues with optional arrays [#1218](https://github.com/mobxjs/mobx-state-tree/issues/1218) through [#1229](https://github.com/mobxjs/mobx-state-tree/pull/1229) by [@xaviergonz](https://github.com/xaviergonz) - Added `getNodeId` to get the internal unique node id for an instance [#1168](https://github.com/mobxjs/mobx-state-tree/issues/1168) through [#1225](https://github.com/mobxjs/mobx-state-tree/pull/1225) by [@xaviergonz](https://github.com/xaviergonz) - Fixed nodes being `pop`/`shift`/`splice` from an array not getting properly destroyed through [#1205](https://github.com/mobxjs/mobx-state-tree/pull/1205) by [@xaviergonz](https://github.com/xaviergonz). Not that this means that in order to access the returned dead nodes data without getting a liveliness error/warning then the returned dead nodes have to be either cloned (`clone`) or their snapshots (`getSnapshot`) have to be used first. # 3.11.0 - Added an optional third argument to `types.optional` that allows to set alternative optional values other than just `undefined` through [#1192](https://github.com/mobxjs/mobx-state-tree/pull/1192) by [@xaviergonz](https://github.com/xaviergonz) - Fixed detaching arrays/maps killing their children [#1173](https://github.com/mobxjs/mobx-state-tree/issues/1173) through [#1175](https://github.com/mobxjs/mobx-state-tree/pull/1175) by [@xaviergonz](https://github.com/xaviergonz) - Added `types.snapshotProcessor` [#947](https://github.com/mobxjs/mobx-state-tree/issues/947) through [#1165](https://github.com/mobxjs/mobx-state-tree/pull/1165) by [@xaviergonz](https://github.com/xaviergonz). This feature will eventually deprecate `postProcessSnapshot` and `preProcessSnapshot` from models in a next major version. - Performance improvement for event handlers so they consume less RAM through [#1160](https://github.com/mobxjs/mobx-state-tree/pull/1160) by [@xaviergonz](https://github.com/xaviergonz) - Make liveliness errors give more info to trace their cause [#1142](https://github.com/mobxjs/mobx-state-tree/issues/1142) through [#1147](https://github.com/mobxjs/mobx-state-tree/pull/1147) by [@xaviergonz](https://github.com/xaviergonz) # 3.10.2 - Fixed a regression regarding json paths not being correctly rooted to the base [#1128](https://github.com/mobxjs/mobx-state-tree/issues/1128) through [#1146](https://github.com/mobxjs/mobx-state-tree/pull/1146) by [@xaviergonz](https://github.com/xaviergonz) # 3.10.1 - Fixed mobx 5.9.0 compatibility [#1143](https://github.com/mobxjs/mobx-state-tree/issues/1143) through [#1144](https://github.com/mobxjs/mobx-state-tree/pull/1144) by [@xaviergonz](https://github.com/xaviergonz) - Made liveliness checking in warn mode log an error so the stack trace can be seen [#1142](https://github.com/mobxjs/mobx-state-tree/issues/1142) through [#1145](https://github.com/mobxjs/mobx-state-tree/pull/1145) by [@xaviergonz](https://github.com/xaviergonz) - Fixed JSON path escaping, where '/' and '~' were incorrectly being encoded/decoded as '~0' and '~1' rather than '~1' and '~0'. Also fixed empty keys not being handled correctly by JSON patches [#1128](https://github.com/mobxjs/mobx-state-tree/issues/1128). Fixed through [#1129](https://github.com/mobxjs/mobx-state-tree/pull/1129) by [@xaviergonz](https://github.com/xaviergonz) # 3.10.0 - Fix for safeReference doesn't work when multiple nodes reference a single reference that gets deleted [#1115](https://github.com/mobxjs/mobx-state-tree/issues/1115) through [#1121](https://github.com/mobxjs/mobx-state-tree/pull/1121) by [@xaviergonz](https://github.com/xaviergonz) - Little fix for `castFlowReturn` not typecasting the promise to its actual result. - Made `isAlive(node)` reactive, so it can be reacted upon through [#1100](https://github.com/mobxjs/mobx-state-tree/pull/1100) by [@xaviergonz](https://github.com/xaviergonz) - Fix for unaccessed nodes not unregistering their identifiers [#1112](https://github.com/mobxjs/mobx-state-tree/issues/1112) through [#1113](https://github.com/mobxjs/mobx-state-tree/pull/1113) by [@xaviergonz](https://github.com/xaviergonz) - Added `clear()` to `UndoManager` middleware through [#1118](https://github.com/mobxjs/mobx-state-tree/pull/1118) by [@chemitaxis](https://github.com/chemitaxis) # 3.9.0 - TypeScript 3.0 or later is now required when using TypeScript. This brings some improvements: - `flow` arguments and return types are now correctly inferred automatically. One exception is when the last return of a `flow` is a `Promise`. In these cases `castFlowReturn(somePromise)` needs to be used so the return type can be inferred properly. - `create` method is now smart enough to warn when no snapshot argument is provided on types that have some mandatory properties. - Added `setLivelinessChecking` and `getLivelinessChecking`, the old `setLivelynessChecking` will eventually be deprecated. - Added `onInvalidated` option for references and `types.safeReference` (see readme) through [#1091](https://github.com/mobxjs/mobx-state-tree/pull/1091) by [@xaviergonz](https://github.com/xaviergonz) - Added `tryReference` and `isValidReference` to use references that might be no longer pointing to any nodes in a safe way through [#1087](https://github.com/mobxjs/mobx-state-tree/pull/1087) by [@xaviergonz](https://github.com/xaviergonz) - Readded `IComplexType` for backwards compatibility. # 3.8.1 - Fixed non-initialized nodes not being destroyed [#1080](https://github.com/mobxjs/mobx-state-tree/issues/1080) through [#1082](https://github.com/mobxjs/mobx-state-tree/pull/1082) by [@k-g-a](https://github.com/k-g-a) - Fixed a memory leak in createActionTrackingMiddleware when using flow [#1083](https://github.com/mobxjs/mobx-state-tree/issues/1083) through [#1084](https://github.com/mobxjs/mobx-state-tree/pull/1084) by [@robinfehr](https://github.com/robinfehr) # 3.8.0 - Added castToSnapshot/castToReferenceSnapshot methods for TypeScript and fixed some TypeScript typings not being properly detected when using SnapshotIn types through [#1074](https://github.com/mobxjs/mobx-state-tree/pull/1074) by [@xaviergonz](https://github.com/xaviergonz) - Fixed redux middleware throwing an error when a flow is called before it is connected [#1065](https://github.com/mobxjs/mobx-state-tree/issues/1065) through [#1079](https://github.com/mobxjs/mobx-state-tree/pull/1079) by [@mkramb](https://github.com/mkramb) and [@xaviergonz](https://github.com/xaviergonz) - Made `addDisposer` return the passed disposer through [#1059](https://github.com/mobxjs/mobx-state-tree/pull/1059) by [@xaviergonz](https://github.com/xaviergonz) # 3.7.1 - Fixed references to nodes being broken after the node was replaced [#1052](https://github.com/mobxjs/mobx-state-tree/issues/1052), plus speed up of reference resolving when using IDs through [#1053](https://github.com/mobxjs/mobx-state-tree/pull/1053) by [@xaviergonz](https://github.com/xaviergonz) # 3.7.0 - Middleware events now also contain `allParentIds` (chain of causing ids, from root until (excluding) current) - Improved redux dev tools integration, now supporting flows and showing action chains through [#1035](https://github.com/mobxjs/mobx-state-tree/pull/1035) based on a fix by [@bourquep](https://github.com/bourquep) # 3.6.0 - Made type Typescript compilation when 'declarations' is set to true + type completion faster thanks to some type optimizations through [#1043](https://github.com/mobxjs/mobx-state-tree/pull/1043) by [@xaviergonz](https://github.com/xaviergonz) - Fix for array reconciliation of union types with ids [#1045](https://github.com/mobxjs/mobx-state-tree/issues/1045) through [#1047](https://github.com/mobxjs/mobx-state-tree/pull/1047) by [@xaviergonz](https://github.com/xaviergonz) - Fixed bug where the eager option for the union type defaulted to true when no options argument was passed but false when it was passed. Now they both default to true when not specified. Fixed through [#1046](https://github.com/mobxjs/mobx-state-tree/pull/1046) by [@xaviergonz](https://github.com/xaviergonz) # 3.5.0 - Fix for afterCreate/afterAttach sometimes throwing an exception when a node was created as part of a view/computed property [#967](https://github.com/mobxjs/mobx-state-tree/issues/967) through [#1026](https://github.com/mobxjs/mobx-state-tree/pull/1026) by [@xaviergonz](https://github.com/xaviergonz). Note that this fix will only work if your installed peer mobx version is >= 4.5.0 or >= 5.5.0 - Fix for cast method being broken in Typescript 3.1.1 through [#1028](https://github.com/mobxjs/mobx-state-tree/pull/1028) by [@xaviergonz](https://github.com/xaviergonz) # 3.4.0 - Added getPropertyMembers(typeOrNode) through [#1016](https://github.com/mobxjs/mobx-state-tree/pull/1016) by [@xaviergonz](https://github.com/xaviergonz) - Fix for preProcessSnapshot not copied on compose [#613](https://github.com/mobxjs/mobx-state-tree/issues/613) through [#1013](https://github.com/mobxjs/mobx-state-tree/pull/1013) by [@theRealScoobaSteve](https://github.com/theRealScoobaSteve) - Fix for actions sometimes failing to resolve this to self through [#1014](https://github.com/mobxjs/mobx-state-tree/pull/1014) by [@xaviergonz](https://github.com/xaviergonz) - Fix for preProcessSnapshot not copied on compose [#613](https://github.com/mobxjs/mobx-state-tree/issues/613) through [#1013](https://github.com/mobxjs/mobx-state-tree/pull/1013) by [@theRealScoobaSteve](https://github.com/theRealScoobaSteve) - Improvements to the bookshop example through [#1009](https://github.com/mobxjs/mobx-state-tree/pull/1009) by [@programmer4web](https://github.com/programmer4web) - Fix for a regression with optional identifiers [#1019](https://github.com/mobxjs/mobx-state-tree/issues/1019) through [#1020](https://github.com/mobxjs/mobx-state-tree/pull/1020) by [@xaviergonz](https://github.com/xaviergonz) # 3.3.0 - Fix for references sometimes not intializing its parents [#993](https://github.com/mobxjs/mobx-state-tree/issues/993) through [#997](https://github.com/mobxjs/mobx-state-tree/pull/997) by [@xaviergonz](https://github.com/xaviergonz) - Fix for TS3 issues with reference type [#994](https://github.com/mobxjs/mobx-state-tree/issues/994) through [#995](https://github.com/mobxjs/mobx-state-tree/pull/995) by [@xaviergonz](https://github.com/xaviergonz) - types.optional will now throw if an instance is directly passed as default value [#1002](https://github.com/mobxjs/mobx-state-tree/issues/1002) through [#1003](https://github.com/mobxjs/mobx-state-tree/pull/1003) by [@xaviergonz](https://github.com/xaviergonz) - Doc fixes and improvements by [@AjaxSolutions](https://github.com/AjaxSolutions) and [@agilgur5](https://github.com/agilgur5) # 3.2.4 - Further improvements for Typescript support for enumeration by [@xaviergonz](https://github.com/xaviergonz) - Smaller generated .d.ts files through [#990](https://github.com/mobxjs/mobx-state-tree/pull/990) by [@xaviergonz](https://github.com/xaviergonz) - Fix for exception when destroying children of types.maybe through [#985](https://github.com/mobxjs/mobx-state-tree/pull/985) by [@dsabanin](https://github.com/dsabanin) # 3.2.3 - Fixed incorrect typing generation for mst-middlewares [#979](https://github.com/mobxjs/mobx-state-tree/issues/979) # 3.2.2 - Fixes for the reconciliation algorithm of arrays [#928](https://github.com/mobxjs/mobx-state-tree/issues/928) through [#960](https://github.com/mobxjs/mobx-state-tree/pull/960) by [@liuqiang1357](https://github.com/liuqiang1357) - Better Typescript support for enumeration, compose, union, literal and references by [@xaviergonz](https://github.com/xaviergonz) - Updated dependencies to latest versions by [@xaviergonz](https://github.com/xaviergonz) - [Internal] Cleanup 'createNode' and related codepaths through [#962](https://github.com/mobxjs/mobx-state-tree/pull/962) by [@k-g-a](https://github.com/k-g-a) # 3.2.1 - Fix for wrong generated TS import [#968](https://github.com/mobxjs/mobx-state-tree/issues/968) through [#969](https://github.com/mobxjs/mobx-state-tree/pull/969) by [@k-g-a](https://github.com/k-g-a) # 3.2.0 - Made the internal CreationType/SnapshotType/Type official via the new [`SnapshotIn`, `SnapshotOut`, `Instance` and `SnapshotOrInstance`](README.md#typeScript-and-mst) by [@xaviergonz](https://github.com/xaviergonz) - A new [`cast` method](README.md#snapshots-can-be-used-to-write-values) that makes automatic casts from instances/input snapshots for assignments by [@xaviergonz](https://github.com/xaviergonz) # 3.1.1 - Fixed typings of `getParent` and `getRoot`. Fixes [#951](https://github.com/mobxjs/mobx-state-tree/issues/951) through [#953](https://github.com/mobxjs/mobx-state-tree/pull/953) by [@xaviergonz](https://github.com/xaviergonz) # 3.1.0 - Fixed issue where snapshot post-processors where not always applied. Fixes [#926](https://github.com/mobxjs/mobx-state-tree/issues/926), [#961](https://github.com/mobxjs/mobx-state-tree/issues/961), through [#959](https://github.com/mobxjs/mobx-state-tree/pull/959) by [@k-g-a](https://github.com/k-g-a) # 3.0.3 - Fixed re-adding the same objects to an array. Fixes [#928](https://github.com/mobxjs/mobx-state-tree/issues/928) through [#949](https://github.com/mobxjs/mobx-state-tree/pull/949) by [@Krivega](https://github.com/Krivega) # 3.0.2 - Introduced `types.integer`! By [@jayarjo](https://github.com/jayarjo) through [#935](https://github.com/mobxjs/mobx-state-tree/pull/935) - Improved typescript typings, several fixes to the type system. Awesome contribution by [@xaviergonz](https://github.com/xaviergonz) through [#937](https://github.com/mobxjs/mobx-state-tree/pull/937) and [#945](https://github.com/mobxjs/mobx-state-tree/pull/945). Fixes [#922](https://github.com/mobxjs/mobx-state-tree/issues/922), [#930](https://github.com/mobxjs/mobx-state-tree/issues/930), [#932](https://github.com/mobxjs/mobx-state-tree/issues/932), [#923](https://github.com/mobxjs/mobx-state-tree/issues/923) - Improved handling of `types.late` # 3.0.1 (retracted) # 3.0.0 Welcome to MobX-state-tree! This version introduces some breaking changes, but nonetheless is an recommended upgrade as all changes should be pretty straight forward and there is no reason anymore to maintain the 2.x range (3.0 is still compatible with MobX 4) ## Most important changes MST 3 is twice as fast in initializing trees with half the memory consumption compared to version 2: Running `yarn speedtest` on Node 9.3: | | MST 2 | MST 3 | | --------------- | ------ | ------ | | Time | 24sec | 12 sec | | Mem | 315MB | 168MB | | Size (min+gzip) | 14.1KB | 15.0KB | Beyond that, MST 3 uses TypeScript 2.8, which results in more accurate TypeScript support. The type system has been simplified and improved in several areas. Several open issues around maps and (numeric) keys have been resolved. The `frozen` type can now be fully typed. See below for the full details. Also, the 'object has died' exception can be suppressed now. One should still address it, but at least it won't be a show-stopper from now on. ## Changes in the type system - **[BREAKING]** `types.identifier` can no longer be parameterized with either `types.string` or `types.number`. So instead of `types.identifier()` use `types.identifier`. Identifiers are now always normalized to strings. This reflects what was already happening internally and solves a lot of edge cases. To use numbers as identifiers, `types.identifierNumber` (instead of `types.identifier(types.number)`) can be used, which serializes it's snapshot to a number, but will internally work like a string based identifier - **[BREAKING]** `types.maybe` now serializes to / from `undefined` by default, as it is more and more the common best practice to don't use `null` at all and MST follows this practice. Use `types.maybeNull` for the old behavior (see [#830](https://github.com/mobxjs/mobx-state-tree/issues/830)) - **[BREAKING]** `types.frozen` is now a function, and can now be invoked in a few different ways: 1. `types.frozen()` - behaves the same as `types.frozen` in MST 2. 2. `types.frozen(SubType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type. Note that the type will not actually be instantiated, so it can only be used to check the _shape_ of the data. Adding views or actions to `SubType` would be pointless. 3. `types.frozen(someDefaultValue)` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field 4. `types.frozen()` - provide a typescript type, to help in strongly typing the field (design time only) - It is no longer necessary to wrap `types.map` or `types.array` in `types.optional` when used in a `model` type, `map` and `array` are now optional by default when used as property type. See [#906](https://github.com/mobxjs/mobx-state-tree/issues/906) - **[BREAKING]** `postProcessSnapshot` can no longer be declared as action, but, like `preProcessSnapshot`, needs to be defined on the type rather than on the instance. - **[BREAKING]** `types.union` is now eager, which means that if multiple valid types for a value are encountered, the first valid type is picked, rather then throwing. #907 / #804, `dispatcher` param => option, ## Other improvements - **[BREAKING]** MobX-state-tree now requires at least TypeScript 2.8 when using MST with typescript. The type system has been revamped, and should now be a lot more accurate, especially concerning snapshot types. - **[BREAKING]** `map.put` will now return the inserted node, rather than the map itself. This makes it easier to find objects for which the identifier is not known upfront. See [#766](https://github.com/mobxjs/mobx-state-tree/issues/766) by [k-g-a](https://github.com/k-g-a) - **[BREAKING]** The order of firing hooks when instantiating has slighlty changed, as the `afterCreate` hook will now only be fired upon instantiation of the tree node, which now happens lazily (on first read / action). The internal order in which hooks are fired within a single node has remained the same. See [#845](https://github.com/mobxjs/mobx-state-tree/issues/845) for details - Significantly improved the performance of constructing MST trees. Significantly reduced the memory footprint of MST. Big shoutout to the relentless effort by [k-g-a](https://github.com/k-g-a) to optimize all the things! See [#845](https://github.com/mobxjs/mobx-state-tree/issues/845) for details. - Introduced `setLivelynessChecking("warn" | "ignore" | "error")`, this can be used to customize how MST should act when one tries to read or write to a node that has already been removed from the tree. The default behavior is `warn`. - Improved the overloads of `model.compose`, see [#892](https://github.com/mobxjs/mobx-state-tree/pull/892) by [t49tran](https://github.com/t49tran) - Fixed issue where computed properties based on `getPath` could return stale results, fixes [#917](https://github.com/mobxjs/mobx-state-tree/issues/917) - Fixed issue where onAction middleware threw on dead nodes when attachAfter option was used - Fixed several issues with maps and numeric identifiers, such as [#884](https://github.com/mobxjs/mobx-state-tree/issues/884) and [#826](https://github.com/mobxjs/mobx-state-tree/issues/826) ## TL,DR Migration guide - `types.identifier(types.number)` => `types.identifierNumber` - `types.identifier()` and `types.identifier(types.string)` =>`types.identifier` - `types.frozen` => `types.frozen()` - `types.maybe(x)` => `types.maybeNull(x)` - `postProcessSnapshot` should now be declared on the type instead of as action # 2.2.0 - Added support for MobX 5. Initiative by [@jeffberry](https://github.com/jeffberry) through [#868](https://github.com/mobxjs/mobx-state-tree/pull/868/files). Please note that there are JavaScript engine restrictions for MobX 5 (no Internet Explorer, or React Native Android). If you need to target those versions please keep using MobX 4 as peer dependency (MST is compatible with both) - Reduced memory footprint with ~10-20%, by [k-g-a](https://github.com/k-g-a) through [#872](https://github.com/mobxjs/mobx-state-tree/pull/872) - Fixed issue where undo manager was not working correctly for non-root stores, by [marcofugaro](https://github.com/marcofugaro) trough [#875](https://github.com/mobxjs/mobx-state-tree/pull/875) # 2.1.0 - Fixed issue where default values of `types.frozen` where not applied correctly after apply snapshot. [#842](https://github.com/mobxjs/mobx-state-tree/pull/842) by [SirbyAlive](https://github.com/SirbyAlive). Fixes [#643](https://github.com/mobxjs/mobx-state-tree/issues/634) - Fixed issue where empty patch sets resulted in in unnecessary history items. [#838](https://github.com/mobxjs/mobx-state-tree/pull/838) by [chemitaxis](https://github.com/chemitaxis). Fixes [#837](https://github.com/mobxjs/mobx-state-tree/issues/837) - `flow`s of destroyed nodes can no 'safely' resume. [#798](https://github.com/mobxjs/mobx-state-tree/pull/798/files) by [Bnaya](https://github.com/Bnaya). Fixes [#792](https://github.com/mobxjs/mobx-state-tree/issues/792) - Made sure the type `Snapshot` is exposed. [#821](https://github.com/mobxjs/mobx-state-tree/pull/821) by [dsabanin](https://github.com/dsabanin) - Fix: the function parameter was incorrectly typed as non-optional. [#851](https://github.com/mobxjs/mobx-state-tree/pull/851) by [abruzzihraig](https://github.com/abruzzihraig) # 2.0.5 - It is now possible to get the snapshot of a node without triggering the `postProcessSnapshot` hook. See [#745](https://github.com/mobxjs/mobx-state-tree/pull/745) for details. By @robinfehr - Introduced `getParentOfType` and `hasParentOfType`. See [#767](https://github.com/mobxjs/mobx-state-tree/pull/767) by @k-g-a - Fixed issue where running `typeCheck` accidentally logged typecheck errors to the console. Fixes [#781](https://github.com/mobxjs/mobx-state-tree/issues/781) # 2.0.4 - Removed accidental dependency on mobx # 2.0.3 - Fixed issue where middleware that changed arguments wasn't properly picked up. See [#732](https://github.com/mobxjs/mobx-state-tree/pull/732) by @robinfehr. Fixes [#731](https://github.com/mobxjs/mobx-state-tree/issues/731) - Fixed reassigning to a custom type from a different type in a union silently failing. See [#737](https://github.com/mobxjs/mobx-state-tree/pull/737) by @univerio. Fixes [#736](https://github.com/mobxjs/mobx-state-tree/issues/736) - Fixed typings issue with TypeScript 2.8. See [#740](https://github.com/mobxjs/mobx-state-tree/pull/740) by @bnaya. - Fixed undo manager apply grouped patches in the wrong order. See [#755](https://github.com/mobxjs/mobx-state-tree/pull/755) by @robinfehr. Fixes [#754](https://github.com/mobxjs/mobx-state-tree/issues/754) # 2.0.2 - Fixed bidirectional references from nodes to nodes, see [#728](https://github.com/mobxjs/mobx-state-tree/pull/728) by @robinfehr - `joinJsonPath` and `splitJsonPath` are now exposed as utilities, see [#724](https://github.com/mobxjs/mobx-state-tree/pull/724) by @jjrv - Several documentation and example fixes # 2.0.1 - Fixed typings for maps of maps [#704](https://github.com/mobxjs/mobx-state-tree/pull/704) by @xaviergonz - Fixed dependency issue in `mst-middlewares` package # 2.0.0 **Breaking changes** - MobX-state-tree now requires MobX 4.0 or higher - Identifiers are now internally always normalized to strings. This also means that adding an object with an number identifier to an observable map, it should still be requested back as string. In general, we recommend to always use string based identifiers to avoid confusion. - Due to the changes in Mobx 4.0, `types.map(subType).keys()` will return `Iterator` instead of `ObservableArrays`. In order to address this issue, wrap the keys with `Array.from()`. # 1.4.0 **Features** - It is now possible to create [custom primitive(like) types](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#custom)! Implements [#673](https://github.com/mobxjs/mobx-state-tree/issues/673) through [#689](https://github.com/mobxjs/mobx-state-tree/pull/689) - [`getIdentifier`](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#getidentifier) is now exposed as function, to get the identifier of a model instance (if any). Fixes [#674](https://github.com/mobxjs/mobx-state-tree/issues/674) through [#678](https://github.com/mobxjs/mobx-state-tree/pull/678) by TimHollies - Writing [middleware](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/middleware.md) has slightly changed, to make it less error prone and more explicit whether a middleware chain should be aborted. For details, see [#675](https://github.com/mobxjs/mobx-state-tree/pull/675) by Robin Fehr - It is now possible to configure whether [attached middleware](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#addmiddleware) should be triggered for the built-in hooks / operations. [#653](https://github.com/mobxjs/mobx-state-tree/pull/653) by Robin Fehr - We exposed an [api](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/API/README.md#getmembers) to perform reflection on model instances. [#649](https://github.com/mobxjs/mobx-state-tree/pull/649) by Robin Fehr **Fixes** - Fixed a bug where items in maps where not properly reconciled when the `put` operation was used. Fixed [#683](https://github.com/mobxjs/mobx-state-tree/issues/683) and [#672](https://github.com/mobxjs/mobx-state-tree/issues/672) through [#693](https://github.com/mobxjs/mobx-state-tree/pull/693) - Fixed issue where trying to resolve a path would throw exceptions. Fixed [#686](https://github.com/mobxjs/mobx-state-tree/issues/686) through [#692](https://github.com/mobxjs/mobx-state-tree/pull/692) - In non production builds actions and views on models can now be replaced, to simplify mocking. Fixes [#646](https://github.com/mobxjs/mobx-state-tree/issues/646) through [#690](https://github.com/mobxjs/mobx-state-tree/pull/690) - Fixed bug where `tryResolve` could leave a node in a corrupt state. [#668](https://github.com/mobxjs/mobx-state-tree/pull/668) by dnakov - Fixed typings for TypeScript 2.7, through [#667](https://github.com/mobxjs/mobx-state-tree/pull/667) by Javier Gonzalez - Several improvements to error messages # 1.3.1 - Fixed bug where `flows` didn't properly batch their next ticks properly in actions, significantly slowing processes down. Fixes [#563](<[#563](https://github.com/mobxjs/mobx-state-tree/issues/563)>) # 1.3.0 - Significantly improved the undo/redo manager. The undo manager now supports groups. See [#504](https://github.com/mobxjs/mobx-state-tree/pull/504) by @robinfehr! See the [updated docs](https://github.com/mobxjs/mobx-state-tree/blob/master/packages/mst-middlewares/README.md#undomanager) for more details. - Significantly improved performance, improvements of 20% could be expected, but changes of course per case. See [#553](https://github.com/mobxjs/mobx-state-tree/pull/553) - Implemented `actionLogger` middleware, which logs most events for async actions - Slightly changed the order in which life cycle hooks are fired. `afterAttach` will no fire first on the parent, then on the children. So, unlike `afterCreate`, in `afterAttach` one can assume in `afterAttach that the parent has completely initialized. # 1.2.1 - 1.2.0 didn't seem to be released correctly... # 1.2.0 - Introduced customizable reference types. See the [reference and identifiers](https://github.com/mobxjs/mobx-state-tree#references-and-identifiers) section. - Introduced `model.volatile` to more easily declare and reuse volatile instance state. Volatile state can contain arbitrary data, is shallowly observable and, like props, cannot be modified without actions. See [`model.volatile`](https://github.com/mobxjs/mobx-state-tree#model-volatile) for more details. # 1.1.1 ### Improvements - Fixed an issue where nodes where not always created correctly, see #534. Should fix #513 and #531. - All tests are now run in both PROD and non PROD configurations, after running into some bugs that only occurred in production builds. - Some internal optimizations have been applied (and many more will follow). Like having internal leaner node for immutable data. See #474 - A lot of minor improvements on the docs # 1.1.0 ### Improvements - The concept of process (asynchronous actions) has been renamed to flows. (Mainly to avoid issues with bundlers) - We changed to a lerna setup which allows separately distributing middleware and testing examples with more ease - Every MST middleware is now shipped in a separate package named `mst-middlewares`. They are now written in TypeScript and fully transpiled to ES5 to avoid problems with uglifyjs in create-react-app bundling. - Introduced `createActionTrackingMiddleware`, this significantly simplifies writing middleware for common scenarios. Especially middleware that deals with asynchronous actions (flows) - Renamed `process` to `flow`. Deprecated `process`. - **BREAKING** As a result some middleware event names have also been changed. If you have custom middlewares this change might affect you. Rename middleware event type prefixes starting with `process` to now start with `flow`. ### Fixes - Fixed nested maps + environments not working correctly, [#447](https://github.com/mobxjs/mobx-state-tree/pull/447) by @xaviergonz - Improved typescript typings for enumerations, up to 50 values are now supported [#424](https://github.com/mobxjs/mobx-state-tree/pull/447) by @danielduwaer # 1.0.2 - Introduced `modelType.extend` which allows creating views and actions with shared state. # 1.0.1 ### Features - Added the middlewares `atomic` and types `TimeTraveller`, `UndoManager`. Check out the [docs](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/middleware.md)! - Introduced `createActionTrackingMiddleware` to simplify the creation of middleware that support complex async processes - exposed `typecheck(type, value)` as public api (will ignore environment flags) ### Improvements - `getEnv` will return an empty object instead of throwing when a tree was initialized without environment - Fixed issue where patches generated for nested maps were incorrect (#396) - Fixed the escaping of (back)slashes in JSON paths (#405) - Improved the algorithm that reconcile items in an array (#384) - Assigning a node that has an environment to a parent is now allowed, as long as the environment is strictly the same (#387) - Many minor documentation improvements. Thanks everybody who created a PR! # 1.0.0 No changes # 0.12.0 - **BREAKING** The redux utilities are no longer part of the core package, but need to be imported from `mobx-state-tree/middleware/redux`. # 0.11.0 ### Breaking changes - **BREAKING** `onAction` middleware no longer throws when encountering unserializable arguments. Rather, it serializes a struct like `{ $MST_UNSERIALIZABLE: true, type: "someType" }`. MST Nodes are no longer automatically serialized. Rather, one should either pass 1: an id, 2: a (relative) path, 3: a snapshot - **BREAKING** `revertPatch` has been dropped. `IReversableJsonPatch` is no longer exposed, instead use the inverse patches generated by `onPatch` - **BREAKING** some middleware events have been renamed: `process_yield` -> `process_resume`, `process_yield_error` -> `process_resume_error`, to make it less confusing how these events relate to `yield` statements. - **BREAKING** patchRecorder's field `patches` has been renamed to `rawPatches,`cleanPatches`to`patches`, and`inversePatches` was added. ### New features - Introduced `decorate(middleware, action)` to easily attach middleware to a specific action - Handlers passed to `onPatch(handler: (patch, inversePatch) => void)` now receive as second argument the inverse patch of the emitted patch - `onAction` lister now supports an `attachAfter` parameter - Middleware events now also contain `parentId` (id of the causing action, `0` if none) and `tree` (the root of context) ### Fixes - ReduxDevTools connection is no longer one step behind [#287](https://github.com/mobxjs/mobx-state-tree/issues/287) - Middleware is no longer run as part of the transaction of the targeted action - Fixed representation of `union` types in error messages # 0.10.3 - **BREAKISH** Redefining lifecycle hooks will now automatically compose them, implements [#252](https://github.com/mobxjs/mobx-state-tree/issues/252) - Added dev-only checks, typecheck will be performed only in dev-mode and top-level API-calls will be checked. - The internal types `IMiddleWareEvent`, `IMiddlewareEventType`, `ISerializedActionCall` are now exposed (fixes [#315](https://github.com/mobxjs/mobx-state-tree/issues/315)) # 0.10.2 - Object model instances no longer share a prototype. # 0.10.1 - Removed accidental dependency on the codemod # 0.10.0 - **BREAKING** the syntax to define model types has been updated. See the [updated docs](https://github.com/mobxjs/mobx-state-tree#creating-models) or the original proposal:[#282](https://github.com/mobxjs/mobx-state-tree/pull/286), but no worries, theres a codemod! :D - **BREAKING** `preProcessSnapshot` hook is no longer a normal hook that can be defined as action. Instead, it should be defined on the type using `types.model(...).preProcessSnapshot(value => value)` - **BREAKING** Asynchronous process should now be defined using `process`. See this [example](https://github.com/mobxjs/mobx-state-tree/blob/adba1943af263898678fe148a80d3d2b9f8dbe63/examples/bookshop/src/stores/BookStore.js#L25) or the [asynchronous action docs](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/async-actions.md). **How to run the codemod?** The codemod is provided as npm package command line tool. It has been written using the TypeScript parser, so it will successfully support either TS or regular JavaScript source files. To run the codemod, you need to first install it globally by `npm install -g mst-codemod-to-0.10`. After that, the `mst-codemod-to-0.10` command will be available in your command line. To perform the codemod, you need to call in your command line `mst-codemod-to-0.10` followed by the filename you want to codemod. A `.bak` file with the original source will be created for backup purposes, and the file you provided will be updated to the new syntax! Have fun! PS: You could also use `npx` instead of installing the codemod globally! :) # 0.9.5 - Asynchronous actions are now a first class concept in mobx-state-tree. See the [docs](https://github.com/mobxjs/mobx-state-tree/blob/master/docs/async-actions.md) # 0.9.4 - Introduced `types.null` and `types.undefined` - Introduced `types.enumeration(name?, options)` # 0.9.3 - Fix `note that a snapshot is compatible` when assigning a type to an optional version of itself - Fix error when deleting a non existing item from a map [#255](https://github.com/mobxjs/mobx-state-tree/issues/255) - Now all required TypeScript interfaces are exported in the main mobx-state-tree package [#256](https://github.com/mobxjs/mobx-state-tree/issues/256) # 0.9.2 Introduced the concept of reverse patches, see [#231](https://github.com/mobxjs/mobx-state-tree/pull/231/) - Introduced the `revertPatch` operation, that takes a patch or list of patches, and reverse applies it to the target. - `onPatch` now takes a second argument, `includeOldValue`, defaulting to `false`, which, if set to true, includes in the patch any value that is being overwritten as result of the patch. Setting this option to true produces patches that can be used with `revertPatch` - `patchRecorder` now introduces additional fields / methods to be able to reverse apply changes: `patchRecorder.cleanPatches`, `patchRecorder.undo` # 0.9.1 - Applying a snapshot or patches will now emit an action as well. The name of the emitted action will be `@APPLY_PATCHES`resp `@APPLY_SNAPSHOT`. See [#107](https://github.com/mobxjs/mobx-state-tree/issues/107) - Fixed issue where same Date instance could'nt be used two times in the same state tree [#229](https://github.com/mobxjs/mobx-state-tree/issues/229) - Fixed issue with reapplying snapshots to Date field resulting in snapshot typecheck error[#233](https://github.com/mobxjs/mobx-state-tree/issues/233) - Declaring `types.maybe(types.frozen)` will now result into an error [#224](https://github.com/mobxjs/mobx-state-tree/issues/224) - Added support for Mobx observable arrays in type checks [#221](https://github.com/mobxjs/mobx-state-tree/issues/221) (from [alessioscalici](https://github.com/alessioscalici)) # 0.9.0 - **BREAKING** Removed `applyPatches` and `applyActions`. Use `applyPatch` resp. `applyAction`, as both will now also accept an array as argument - **BREAKING** `unprotect` and `protect` can only be applied at root nodes to avoid confusing scenarios Fixed [#180](https://github.com/mobxjs/mobx-state-tree/issues/180) - Fixed [#141](https://github.com/mobxjs/mobx-state-tree/issues/141), actions / views are no longer wrapped in dynamically generated functions for a better debugging experience - Small improvements to typings, fixed compilation issues with TypeScript 2.4.1. - Fixed issues where `compose` couldn't overwrite getters. [#209](https://github.com/mobxjs/mobx-state-tree/issues/209), by @homura - Fixed CDN links in readme - Added TodoMVC to the examples section # 0.8.2 - Fixed issue in rollup module bundle # 0.8.1 - Fixed issue in release script, rendering 0.8.0 useless # 0.8.0 - **BREAKING** Dropped `types.extend` in favor of `types.compose`. See [#192](https://github.com/mobxjs/mobx-state-tree/issues/192) - Introduced the lifecycle hooks `preProcessSnapshot` and `postProcessSnapshot`. See [#203](https://github.com/mobxjs/mobx-state-tree/pull/203) / [#100](https://github.com/mobxjs/mobx-state-tree/issues/100) - Use rollup as bundler [#196](https://github.com/mobxjs/mobx-state-tree/pull/196) # 0.7.3 - Introduced the concept of volatile / local state in models. See [#168](https://github.com/mobxjs/mobx-state-tree/issues/168), or [docs](https://github.com/mobxjs/mobx-state-tree/tree/master#volatile-state) - Fixed issue with types.map() with types.identifier(types.number) [#191](https://github.com/mobxjs/mobx-state-tree/issues/191) reported by @boatkorachal - Fixed issue with reconciler that affected types.map when node already existed at that key reported by @boatkorachal [#191](https://github.com/mobxjs/mobx-state-tree/issues/191) # 0.7.2 - Fixed `cannot read property resolve of undefined` thanks to @cpunion for reporting, now value of dead nodes will be undefined. [#186](https://github.com/mobxjs/mobx-state-tree/issues/186) - Fixed `[LateType] is not defined` thanks to @amir-arad for reporting, when using late as model property type [#187](https://github.com/mobxjs/mobx-state-tree/issues/187) - Fixed `Object.freeze can only be called on Object` thanks to @ds300 for reporting, when using MST on a ReactNative environment [#189](https://github.com/mobxjs/mobx-state-tree/issues/189) - Now the entire codebase is prettier! :D [#187](https://github.com/mobxjs/mobx-state-tree/issues/187) # 0.7.1 - Fixed `array.remove` not working # 0.7.0 The type system and internal administration has been refactoring, making the internals both simpler and more flexible. Things like references and identifiers are now first class types, making them much better composable. [#152](https://github.com/mobxjs/mobx-state-tree/issues/152) - **BREAKING** References with a predefined lookup path are no longer supported. Instead of that, identifiers are now looked up in the entire tree. For that reasons identifiers now have to be unique in the entire tree, per type. - **BREAKING** `resolve` is renamed to `resolvePath` - Introduced `resolveIdentifier(type, tree, identifier)` to find objects by identifier - **BREAKING** `types.reference` is by default non-nullable. For nullable identifiers, use `types.maybe(types.reference(X))` - Many, many improvements. Related open issues will be updated. - **BREAKING** `isMST` is renamed to `isStateTreeNode` # 0.6.3 - Fixed issue with array/maps of union types @abruzzihraig [#151](https://github.com/mobxjs/mobx-state-tree/issues/151) - Make types.extend support computed attributes @cpunion [#169](https://github.com/mobxjs/mobx-state-tree/issues/169) - Fixed issue with map of primitive types and applySnapshot @pioh [#155](https://github.com/mobxjs/mobx-state-tree/issues/155) - Better type declarations for union, up to 10 supported types # 0.6.2 - Fixed issue where arrays where not properly serialized as action argument # 0.6.1 - Improved reporting of Type.is(), now it returns a fine grained report of why the provided value is not applicable. ``` [mobx-state-tree] Error while converting [{}] to AnonymousModel[]: at path "/name" snapshot undefined is not assignable to type: string. at path "/quantity" snapshot undefined is not assignable to type: number. ``` - Fixed support for `types.reference` in combination with `types.late`, by @robinfehr # 0.6.0 - **BREAKING** `types.withDefault` has been renamed to `types.optional` - **BREAKING** Array and map types can no longer be left out of snapshots by default. Use `optional` to make them optional in the snapshot - **BREAKING** Literals no longer have a default value by default (use optional + literal instead) - **BREAKING** Disabled inlining type.model definitions as introduced in 0.5.1; to many subtle issues - Improved identifier support, they are no properly propagated through utility types like `maybe`, `union` etc - Fixed issue where fields where not referred back to default when a partial snapshot was provided - Fixed #122: `types.identifier` now also accepts a subtype to override the default string type; e.g. `types.identifier(types.number)` # 0.5.1 - Introduced support for lazy evaluating values in `withDefault`, useful to generate UUID's, timestamps or non-primitive default values - ~~It is now possible to define something like~~ Removed in 0.6.0 ```javascript const Box = types.model({ point: { x: 10, y: 10 } } ``` Where the type of `point` property is inferred to `point: types.withDefault(types.model({ x: 10, y: 10}), () => ({ x: 10, y: 10 }))` # 0.5.0 - ** BREAKING ** protection is now enabled by default (#101) - ** BREAKING ** it is no longer possible to read values from a dead object. Except through `getSnapshot` or `clone` (#102) - ** BREAKING ** `types.recursive` has been removed in favor of `types.late` - Introduced `unprotect`, to disable protection mode for a certain instance. Useful in `afterCreate` hooks - Introduced `types.late`. Usage: `types.late(() => typeDefinition)`. Can be used for circular / recursive type definitions, even across files. See `test/circular(1|2).ts` for an example (#74) # 0.4.0 **BREAKING** `types.model` no requires 2 parameters to define a model. The first parameter defines the properties, derived values and view functions. The second argument is used to define the actions. For example: ```javascript const Todo = types.model("Todo", { done: types.boolean, toggle() { this.done = !this.done } }) ``` Now should be defined as: ```javascript const Todo = types.model( "Todo", { done: types.boolean }, { toggle() { this.done = !this.done } } ) ``` It is still possible to define functions on the first object. However, those functions are not considered to be actions, but views. They are not allowed to modify values, but instead should produce a new value themselves. # 0.3.3 - Introduced lifecycle hooks `afterCreate`, `afterAttach`, `beforeDetach`, `beforeDestroy`, implements #76 - Introduced the convenience method `addDisposer(this, cb)` that can be used to easily destruct reactions etc. which are set up in `afterCreate`. See #76 # 0.3.2 - Fix: actions where not bound automatically - Improved and simplified the reconciliation mechanism, fixed many edge cases - Improved the reference mechanism, fixed many edge cases - Improved performance # 0.3.1 - (re) introduced the concept of environments, which can be passed as second argument to `.create`, and picked up using `getEnv` # 0.3.0 - Removed `primitive` type, use a more specific type instead - Improved typescript typings of snapshots - Added `depth` parameter to `getParent` and `hasParent` - Separated the concepts of middleware and serializable actions. It is now possible to intercept, modify actions etc through `addMiddleWare`. `onAction` now uses middleware, if it is used, all parameters of actions should be serializable! # 0.2.2 - Introduced the concept of liveliness; if nodes are removed from the the tree because they are replaced by some other value, they will be marked as "died". This should help to early signal when people hold on to references that are not part of the tree anymore. To explicitly remove an node from a tree, with the intent to spawn a new state tree from it, use `detach`. - Introduced the convenience method `destroy` to remove a model from it's parent and mark it as dead. - Introduced the concept of protected trees. If a tree is protected using `protect`, it can only be modified through action, and not by mutating it directly anymore. # 0.2.1 - Introduced .Type and .SnapshotType to be used with TypeScript to get the type for a model # 0.2.0 - Renamed `createFactory` to `types.model` (breaking!) - Renamed `composeFactory` to `types.extend` (breaking!) - Actions should now be declared as `name(params) { body }`, instead of `name: action(function (params) { body})` (breaking!) - Models are no longer constructed by invoking the factory as function, but by calling `factory.create` (breaking!) - Introduced `identifier` - Introduced / improved `reference` - Greatly improved typescript support, type inference etc. However there are still limitations as the full typesystem of MST cannot be expressed in TypeScript. Especially concerning the type of snapshots and the possibility to use snapshots as first class value. ================================================ FILE: docker-compose.yml ================================================ version: "3" services: docusaurus: build: . ports: - 3000:3000 - 35729:35729 volumes: - ./docs:/app/docs - ./website/blog:/app/website/blog - ./website/core:/app/website/core - ./website/i18n:/app/website/i18n - ./website/pages:/app/website/pages - ./website/static:/app/website/static - ./website/sidebars.json:/app/website/sidebars.json - ./website/siteConfig.js:/app/website/siteConfig.js working_dir: /app/website ================================================ FILE: docs/.gitattributes ================================================ /API/**/* -diff -merge /API/**/* linguist-generated ================================================ FILE: docs/API/index.md ================================================ --- id: "index" title: "mobx-state-tree - v7.0.2" sidebar_label: "Globals" --- [mobx-state-tree - v7.0.2](index.md) ## Index ### Interfaces * [CustomTypeOptions](interfaces/customtypeoptions.md) * [FunctionWithFlag](interfaces/functionwithflag.md) * [IActionContext](interfaces/iactioncontext.md) * [IActionRecorder](interfaces/iactionrecorder.md) * [IActionTrackingMiddleware2Call](interfaces/iactiontrackingmiddleware2call.md) * [IActionTrackingMiddleware2Hooks](interfaces/iactiontrackingmiddleware2hooks.md) * [IActionTrackingMiddlewareHooks](interfaces/iactiontrackingmiddlewarehooks.md) * [IAnyComplexType](interfaces/ianycomplextype.md) * [IAnyModelType](interfaces/ianymodeltype.md) * [IAnyType](interfaces/ianytype.md) * [IHooks](interfaces/ihooks.md) * [IJsonPatch](interfaces/ijsonpatch.md) * [IMiddlewareEvent](interfaces/imiddlewareevent.md) * [IModelReflectionData](interfaces/imodelreflectiondata.md) * [IModelReflectionPropertiesData](interfaces/imodelreflectionpropertiesdata.md) * [IModelType](interfaces/imodeltype.md) * [IPatchRecorder](interfaces/ipatchrecorder.md) * [IReversibleJsonPatch](interfaces/ireversiblejsonpatch.md) * [ISerializedActionCall](interfaces/iserializedactioncall.md) * [ISimpleType](interfaces/isimpletype.md) * [ISnapshotProcessor](interfaces/isnapshotprocessor.md) * [ISnapshotProcessors](interfaces/isnapshotprocessors.md) * [IType](interfaces/itype.md) * [IValidationContextEntry](interfaces/ivalidationcontextentry.md) * [IValidationError](interfaces/ivalidationerror.md) * [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md) * [ReferenceOptionsOnInvalidated](interfaces/referenceoptionsoninvalidated.md) * [UnionOptions](interfaces/unionoptions.md) ### Type aliases * [IDisposer](index.md#idisposer) * [IHooksGetter](index.md#ihooksgetter) * [IMiddlewareEventType](index.md#imiddlewareeventtype) * [IMiddlewareHandler](index.md#imiddlewarehandler) * [ITypeDispatcher](index.md#itypedispatcher) * [IUnionType](index.md#iuniontype) * [IValidationContext](index.md#ivalidationcontext) * [IValidationResult](index.md#ivalidationresult) * [Instance](index.md#instance) * [LivelinessMode](index.md#livelinessmode) * [OnReferenceInvalidated](index.md#onreferenceinvalidated) * [OnReferenceInvalidatedEvent](index.md#onreferenceinvalidatedevent) * [ReferenceIdentifier](index.md#referenceidentifier) * [ReferenceOptions](index.md#referenceoptions) * [SnapshotIn](index.md#snapshotin) * [SnapshotOrInstance](index.md#snapshotorinstance) * [SnapshotOut](index.md#snapshotout) ### Variables * [DatePrimitive](index.md#const-dateprimitive) * [boolean](index.md#const-boolean) * [finite](index.md#const-finite) * [float](index.md#const-float) * [identifier](index.md#const-identifier) * [identifierNumber](index.md#const-identifiernumber) * [integer](index.md#const-integer) * [nullType](index.md#const-nulltype) * [number](index.md#const-number) * [string](index.md#const-string) * [undefinedType](index.md#const-undefinedtype) ### Functions * [addDisposer](index.md#adddisposer) * [addMiddleware](index.md#addmiddleware) * [applyAction](index.md#applyaction) * [applyPatch](index.md#applypatch) * [applySnapshot](index.md#applysnapshot) * [array](index.md#array) * [cast](index.md#cast) * [castFlowReturn](index.md#castflowreturn) * [castToReferenceSnapshot](index.md#casttoreferencesnapshot) * [castToSnapshot](index.md#casttosnapshot) * [clone](index.md#clone) * [compose](index.md#compose) * [createActionTrackingMiddleware](index.md#createactiontrackingmiddleware) * [createActionTrackingMiddleware2](index.md#createactiontrackingmiddleware2) * [custom](index.md#custom) * [decorate](index.md#decorate) * [destroy](index.md#destroy) * [detach](index.md#detach) * [enumeration](index.md#enumeration) * [escapeJsonPath](index.md#escapejsonpath) * [flow](index.md#flow) * [frozen](index.md#frozen) * [getChildType](index.md#getchildtype) * [getEnv](index.md#getenv) * [getIdentifier](index.md#getidentifier) * [getLivelinessChecking](index.md#getlivelinesschecking) * [getMembers](index.md#getmembers) * [getNodeId](index.md#getnodeid) * [getParent](index.md#getparent) * [getParentOfType](index.md#getparentoftype) * [getPath](index.md#getpath) * [getPathParts](index.md#getpathparts) * [getPropertyMembers](index.md#getpropertymembers) * [getRelativePath](index.md#getrelativepath) * [getRoot](index.md#getroot) * [getRunningActionContext](index.md#getrunningactioncontext) * [getSnapshot](index.md#getsnapshot) * [getType](index.md#gettype) * [hasEnv](index.md#hasenv) * [hasParent](index.md#hasparent) * [hasParentOfType](index.md#hasparentoftype) * [isActionContextChildOf](index.md#isactioncontextchildof) * [isActionContextThisOrChildOf](index.md#isactioncontextthisorchildof) * [isAlive](index.md#isalive) * [isArrayType](index.md#isarraytype) * [isFrozenType](index.md#isfrozentype) * [isIdentifierType](index.md#isidentifiertype) * [isLateType](index.md#islatetype) * [isLiteralType](index.md#isliteraltype) * [isMapType](index.md#ismaptype) * [isModelType](index.md#ismodeltype) * [isOptionalType](index.md#isoptionaltype) * [isPrimitiveType](index.md#isprimitivetype) * [isProtected](index.md#isprotected) * [isReferenceType](index.md#isreferencetype) * [isRefinementType](index.md#isrefinementtype) * [isRoot](index.md#isroot) * [isStateTreeNode](index.md#isstatetreenode) * [isType](index.md#istype) * [isUnionType](index.md#isuniontype) * [isValidReference](index.md#isvalidreference) * [joinJsonPath](index.md#joinjsonpath) * [late](index.md#late) * [lazy](index.md#lazy) * [literal](index.md#literal) * [map](index.md#map) * [maybe](index.md#maybe) * [maybeNull](index.md#maybenull) * [model](index.md#model) * [onAction](index.md#onaction) * [onPatch](index.md#onpatch) * [onSnapshot](index.md#onsnapshot) * [optional](index.md#optional) * [protect](index.md#protect) * [recordActions](index.md#recordactions) * [recordPatches](index.md#recordpatches) * [reference](index.md#reference) * [refinement](index.md#refinement) * [resolveIdentifier](index.md#resolveidentifier) * [resolvePath](index.md#resolvepath) * [safeReference](index.md#safereference) * [setLivelinessChecking](index.md#setlivelinesschecking) * [snapshotProcessor](index.md#snapshotprocessor) * [splitJsonPath](index.md#splitjsonpath) * [toGenerator](index.md#togenerator) * [toGeneratorFunction](index.md#togeneratorfunction) * [tryReference](index.md#tryreference) * [tryResolve](index.md#tryresolve) * [typecheck](index.md#typecheck) * [unescapeJsonPath](index.md#unescapejsonpath) * [union](index.md#union) * [unprotect](index.md#unprotect) * [walk](index.md#walk) ### Object literals * [types](index.md#const-types) ## Type aliases ### IDisposer Ƭ **IDisposer**: *function* *Defined in [src/utils.ts:35](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/utils.ts#L35)* A generic disposer. #### Type declaration: ▸ (): *void* ___ ### IHooksGetter Ƭ **IHooksGetter**: *function* *Defined in [src/core/node/Hook.ts:19](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/Hook.ts#L19)* #### Type declaration: ▸ (`self`: T): *[IHooks](interfaces/ihooks.md)* **Parameters:** Name | Type | ------ | ------ | `self` | T | ___ ### IMiddlewareEventType Ƭ **IMiddlewareEventType**: *"action" | "flow_spawn" | "flow_resume" | "flow_resume_error" | "flow_return" | "flow_throw"* *Defined in [src/core/action.ts:16](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L16)* ___ ### IMiddlewareHandler Ƭ **IMiddlewareHandler**: *function* *Defined in [src/core/action.ts:54](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L54)* #### Type declaration: ▸ (`actionCall`: [IMiddlewareEvent](interfaces/imiddlewareevent.md), `next`: function, `abort`: function): *any* **Parameters:** ▪ **actionCall**: *[IMiddlewareEvent](interfaces/imiddlewareevent.md)* ▪ **next**: *function* ▸ (`actionCall`: [IMiddlewareEvent](interfaces/imiddlewareevent.md), `callback?`: undefined | function): *void* **Parameters:** Name | Type | ------ | ------ | `actionCall` | [IMiddlewareEvent](interfaces/imiddlewareevent.md) | `callback?` | undefined | function | ▪ **abort**: *function* ▸ (`value`: any): *void* **Parameters:** Name | Type | ------ | ------ | `value` | any | ___ ### ITypeDispatcher Ƭ **ITypeDispatcher**: *function* *Defined in [src/types/utility-types/union.ts:22](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L22)* #### Type declaration: ▸ (`snapshot`: Types[number]["SnapshotType"]): *Types[number]* **Parameters:** Name | Type | ------ | ------ | `snapshot` | Types[number]["SnapshotType"] | ___ ### IUnionType Ƭ **IUnionType**: *ITypeUnion‹Types[number]["CreationType"], Types[number]["SnapshotType"], Types[number]["TypeWithoutSTN"]›* *Defined in [src/types/utility-types/union.ts:169](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L169)* ___ ### IValidationContext Ƭ **IValidationContext**: *[IValidationContextEntry](interfaces/ivalidationcontextentry.md)[]* *Defined in [src/core/type/type-checker.ts:23](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L23)* Array of validation context entries ___ ### IValidationResult Ƭ **IValidationResult**: *[IValidationError](interfaces/ivalidationerror.md)[]* *Defined in [src/core/type/type-checker.ts:36](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L36)* Type validation result, which is an array of type validation errors ___ ### Instance Ƭ **Instance**: *T extends object ? T["Type"] : T* *Defined in [src/core/type/type.ts:233](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L233)* The instance representation of a given type. ___ ### LivelinessMode Ƭ **LivelinessMode**: *"warn" | "error" | "ignore"* *Defined in [src/core/node/livelinessChecking.ts:7](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/livelinessChecking.ts#L7)* Defines what MST should do when running into reads / writes to objects that have died. - `"warn"`: Print a warning (default). - `"error"`: Throw an exception. - "`ignore`": Do nothing. ___ ### OnReferenceInvalidated Ƭ **OnReferenceInvalidated**: *function* *Defined in [src/types/utility-types/reference.ts:45](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L45)* #### Type declaration: ▸ (`event`: [OnReferenceInvalidatedEvent](index.md#onreferenceinvalidatedevent)‹STN›): *void* **Parameters:** Name | Type | ------ | ------ | `event` | [OnReferenceInvalidatedEvent](index.md#onreferenceinvalidatedevent)‹STN› | ___ ### OnReferenceInvalidatedEvent Ƭ **OnReferenceInvalidatedEvent**: *object* *Defined in [src/types/utility-types/reference.ts:36](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L36)* #### Type declaration: ___ ### ReferenceIdentifier Ƭ **ReferenceIdentifier**: *string | number* *Defined in [src/types/utility-types/identifier.ts:147](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/identifier.ts#L147)* Valid types for identifiers. ___ ### ReferenceOptions Ƭ **ReferenceOptions**: *[ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› | [ReferenceOptionsOnInvalidated](interfaces/referenceoptionsoninvalidated.md)‹IT› | [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› & [ReferenceOptionsOnInvalidated](interfaces/referenceoptionsoninvalidated.md)‹IT›* *Defined in [src/types/utility-types/reference.ts:481](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L481)* ___ ### SnapshotIn Ƭ **SnapshotIn**: *T extends object ? T["CreationType"] : T extends IStateTreeNode ? IT["CreationType"] : T* *Defined in [src/core/type/type.ts:238](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L238)* The input (creation) snapshot representation of a given type. ___ ### SnapshotOrInstance Ƭ **SnapshotOrInstance**: *[SnapshotIn](index.md#snapshotin)‹T› | [Instance](index.md#instance)‹T›* *Defined in [src/core/type/type.ts:279](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L279)* A type which is equivalent to the union of SnapshotIn and Instance types of a given typeof TYPE or typeof VARIABLE. For primitives it defaults to the primitive itself. For example: - `SnapshotOrInstance = SnapshotIn | Instance` - `SnapshotOrInstance = SnapshotIn | Instance` Usually you might want to use this when your model has a setter action that sets a property. Example: ```ts const ModelA = types.model({ n: types.number }) const ModelB = types.model({ innerModel: ModelA }).actions(self => ({ // this will accept as property both the snapshot and the instance, whichever is preferred setInnerModel(m: SnapshotOrInstance) { self.innerModel = cast(m) } })) ``` ___ ### SnapshotOut Ƭ **SnapshotOut**: *T extends object ? T["SnapshotType"] : T extends IStateTreeNode ? IT["SnapshotType"] : T* *Defined in [src/core/type/type.ts:247](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L247)* The output snapshot representation of a given type. ## Variables ### `Const` DatePrimitive • **DatePrimitive**: *[IType](interfaces/itype.md)‹number | Date, number, Date›* = _DatePrimitive *Defined in [src/types/primitives.ts:215](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L215)* `types.Date` - Creates a type that can only contain a javascript Date value. Example: ```ts const LogLine = types.model({ timestamp: types.Date, }) LogLine.create({ timestamp: new Date() }) ``` ___ ### `Const` bigint • **bigint**: *[IType](interfaces/itype.md)‹bigint | string | number, string, bigint›* = _BigIntPrimitive *Defined in [src/types/primitives.ts:192](https://github.com/mobxjs/mobx-state-tree/blob/8c6f719b/src/types/primitives.ts#L192)* `types.bigint` - Creates a type that can only contain a bigint value. Snapshots serialize to string (JSON-safe) and deserialize from string, number or bigint. Example: ```ts const BigId = types.model({ id: types.identifier, value: types.bigint }) getSnapshot(store).value // "0" (string, JSON-safe) ``` ___ ### `Const` boolean • **boolean**: *[ISimpleType](interfaces/isimpletype.md)‹boolean›* = new CoreType( "boolean", TypeFlags.Boolean, v => typeof v === "boolean" ) *Defined in [src/types/primitives.ts:169](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L169)* `types.boolean` - Creates a type that can only contain a boolean value. This type is used for boolean values by default Example: ```ts const Thing = types.model({ isCool: types.boolean, isAwesome: false }) ``` ___ ### `Const` finite • **finite**: *[ISimpleType](interfaces/isimpletype.md)‹number›* = new CoreType( "finite", TypeFlags.Finite, v => isFinite(v) ) *Defined in [src/types/primitives.ts:150](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L150)* `types.finite` - Creates a type that can only contain an finite value. Example: ```ts const Size = types.model({ width: types.finite, height: 10 }) ``` ___ ### `Const` float • **float**: *[ISimpleType](interfaces/isimpletype.md)‹number›* = new CoreType( "float", TypeFlags.Float, v => isFloat(v) ) *Defined in [src/types/primitives.ts:132](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L132)* `types.float` - Creates a type that can only contain an float value. Example: ```ts const Size = types.model({ width: types.float, height: 10 }) ``` ___ ### `Const` identifier • **identifier**: *[ISimpleType](interfaces/isimpletype.md)‹string›* = new IdentifierType() *Defined in [src/types/utility-types/identifier.ts:115](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/identifier.ts#L115)* `types.identifier` - Identifiers are used to make references, lifecycle events and reconciling works. Inside a state tree, for each type can exist only one instance for each given identifier. For example there couldn't be 2 instances of user with id 1. If you need more, consider using references. Identifier can be used only as type property of a model. This type accepts as parameter the value type of the identifier field that can be either string or number. Example: ```ts const Todo = types.model("Todo", { id: types.identifier, title: types.string }) ``` **`returns`** ___ ### `Const` identifierNumber • **identifierNumber**: *[ISimpleType](interfaces/isimpletype.md)‹number›* = new IdentifierNumberType() *Defined in [src/types/utility-types/identifier.ts:130](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/identifier.ts#L130)* `types.identifierNumber` - Similar to `types.identifier`. This one will serialize from / to a number when applying snapshots Example: ```ts const Todo = types.model("Todo", { id: types.identifierNumber, title: types.string }) ``` **`returns`** ___ ### `Const` integer • **integer**: *[ISimpleType](interfaces/isimpletype.md)‹number›* = new CoreType( "integer", TypeFlags.Integer, v => isInteger(v) ) *Defined in [src/types/primitives.ts:114](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L114)* `types.integer` - Creates a type that can only contain an integer value. Example: ```ts const Size = types.model({ width: types.integer, height: 10 }) ``` ___ ### `Const` nullType • **nullType**: *[ISimpleType](interfaces/isimpletype.md)‹null›* = new CoreType( "null", TypeFlags.Null, v => v === null ) *Defined in [src/types/primitives.ts:178](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L178)* `types.null` - The type of the value `null` ___ ### `Const` number • **number**: *[ISimpleType](interfaces/isimpletype.md)‹number›* = new CoreType( "number", TypeFlags.Number, v => typeof v === "number" ) *Defined in [src/types/primitives.ts:96](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L96)* `types.number` - Creates a type that can only contain a numeric value. This type is used for numeric values by default Example: ```ts const Vector = types.model({ x: types.number, y: 1.5 }) ``` ___ ### `Const` string • **string**: *[ISimpleType](interfaces/isimpletype.md)‹string›* = new CoreType( "string", TypeFlags.String, v => typeof v === "string" ) *Defined in [src/types/primitives.ts:77](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L77)* `types.string` - Creates a type that can only contain a string value. This type is used for string values by default Example: ```ts const Person = types.model({ firstName: types.string, lastName: "Doe" }) ``` ___ ### `Const` undefinedType • **undefinedType**: *[ISimpleType](interfaces/isimpletype.md)‹undefined›* = new CoreType( "undefined", TypeFlags.Undefined, v => v === undefined ) *Defined in [src/types/primitives.ts:187](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L187)* `types.undefined` - The type of the value `undefined` ## Functions ### addDisposer ▸ **addDisposer**(`target`: IAnyStateTreeNode, `disposer`: [IDisposer](index.md#idisposer)): *[IDisposer](index.md#idisposer)* *Defined in [src/core/mst-operations.ts:751](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L751)* Use this utility to register a function that should be called whenever the targeted state tree node is destroyed. This is a useful alternative to managing cleanup methods yourself using the `beforeDestroy` hook. This methods returns the same disposer that was passed as argument. Example: ```ts const Todo = types.model({ title: types.string }).actions(self => ({ afterCreate() { const autoSaveDisposer = reaction( () => getSnapshot(self), snapshot => sendSnapshotToServerSomehow(snapshot) ) // stop sending updates to server if this // instance is destroyed addDisposer(self, autoSaveDisposer) } })) ``` **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | `disposer` | [IDisposer](index.md#idisposer) | **Returns:** *[IDisposer](index.md#idisposer)* The same disposer that was passed as argument ___ ### addMiddleware ▸ **addMiddleware**(`target`: IAnyStateTreeNode, `handler`: [IMiddlewareHandler](index.md#imiddlewarehandler), `includeHooks`: boolean): *[IDisposer](index.md#idisposer)* *Defined in [src/core/action.ts:174](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L174)* Middleware can be used to intercept any action is invoked on the subtree where it is attached. If a tree is protected (by default), this means that any mutation of the tree will pass through your middleware. For more details, see the [middleware docs](concepts/middleware.md) **Parameters:** Name | Type | Default | Description | ------ | ------ | ------ | ------ | `target` | IAnyStateTreeNode | - | Node to apply the middleware to. | `handler` | [IMiddlewareHandler](index.md#imiddlewarehandler) | - | - | `includeHooks` | boolean | true | - | **Returns:** *[IDisposer](index.md#idisposer)* A callable function to dispose the middleware. ___ ### applyAction ▸ **applyAction**(`target`: IAnyStateTreeNode, `actions`: [ISerializedActionCall](interfaces/iserializedactioncall.md) | [ISerializedActionCall](interfaces/iserializedactioncall.md)[]): *void* *Defined in [src/middlewares/on-action.ts:89](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L89)* Applies an action or a series of actions in a single MobX transaction. Does not return any value Takes an action description as produced by the `onAction` middleware. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `target` | IAnyStateTreeNode | - | `actions` | [ISerializedActionCall](interfaces/iserializedactioncall.md) | [ISerializedActionCall](interfaces/iserializedactioncall.md)[] | | **Returns:** *void* ___ ### applyPatch ▸ **applyPatch**(`target`: IAnyStateTreeNode, `patch`: [IJsonPatch](interfaces/ijsonpatch.md) | ReadonlyArray‹[IJsonPatch](interfaces/ijsonpatch.md)›): *void* *Defined in [src/core/mst-operations.ts:124](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L124)* Applies a JSON-patch to the given model instance or bails out if the patch couldn't be applied See [patches](https://github.com/mobxjs/mobx-state-tree#patches) for more details. Can apply a single past, or an array of patches. **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | `patch` | [IJsonPatch](interfaces/ijsonpatch.md) | ReadonlyArray‹[IJsonPatch](interfaces/ijsonpatch.md)› | **Returns:** *void* ___ ### applySnapshot ▸ **applySnapshot**<**C**>(`target`: IStateTreeNode‹[IType](interfaces/itype.md)‹C, any, any››, `snapshot`: C): *void* *Defined in [src/core/mst-operations.ts:321](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L321)* Applies a snapshot to a given model instances. Patch and snapshot listeners will be invoked as usual. **Type parameters:** ▪ **C** **Parameters:** Name | Type | ------ | ------ | `target` | IStateTreeNode‹[IType](interfaces/itype.md)‹C, any, any›› | `snapshot` | C | **Returns:** *void* ___ ### array ▸ **array**<**IT**>(`subtype`: IT): *IArrayType‹IT›* *Defined in [src/types/complex-types/array.ts:348](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/array.ts#L348)* `types.array` - Creates an index based collection type who's children are all of a uniform declared type. This type will always produce [observable arrays](https://mobx.js.org/api.html#observablearray) Example: ```ts const Todo = types.model({ task: types.string }) const TodoStore = types.model({ todos: types.array(Todo) }) const s = TodoStore.create({ todos: [] }) unprotect(s) // needed to allow modifying outside of an action s.todos.push({ task: "Grab coffee" }) console.log(s.todos[0]) // prints: "Grab coffee" ``` **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** Name | Type | ------ | ------ | `subtype` | IT | **Returns:** *IArrayType‹IT›* ___ ### cast ▸ **cast**<**O**>(`snapshotOrInstance`: O): *O* *Defined in [src/core/mst-operations.ts:908](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L908)* Casts a node snapshot or instance type to an instance type so it can be assigned to a type instance. Note that this is just a cast for the type system, this is, it won't actually convert a snapshot to an instance, but just fool typescript into thinking so. Either way, casting when outside an assignation operation won't compile. Example: ```ts const ModelA = types.model({ n: types.number }).actions(self => ({ setN(aNumber: number) { self.n = aNumber } })) const ModelB = types.model({ innerModel: ModelA }).actions(self => ({ someAction() { // this will allow the compiler to assign a snapshot to the property self.innerModel = cast({ a: 5 }) } })) ``` **Type parameters:** ▪ **O**: *string | number | boolean | null | undefined* **Parameters:** Name | Type | Description | ------ | ------ | ------ | `snapshotOrInstance` | O | Snapshot or instance | **Returns:** *O* The same object cast as an instance ▸ **cast**<**O**>(`snapshotOrInstance`: TypeOfValue["CreationType"] | TypeOfValue["SnapshotType"] | TypeOfValue["Type"]): *O* *Defined in [src/core/mst-operations.ts:911](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L911)* Casts a node snapshot or instance type to an instance type so it can be assigned to a type instance. Note that this is just a cast for the type system, this is, it won't actually convert a snapshot to an instance, but just fool typescript into thinking so. Either way, casting when outside an assignation operation won't compile. Example: ```ts const ModelA = types.model({ n: types.number }).actions(self => ({ setN(aNumber: number) { self.n = aNumber } })) const ModelB = types.model({ innerModel: ModelA }).actions(self => ({ someAction() { // this will allow the compiler to assign a snapshot to the property self.innerModel = cast({ a: 5 }) } })) ``` **Type parameters:** ▪ **O** **Parameters:** Name | Type | Description | ------ | ------ | ------ | `snapshotOrInstance` | TypeOfValue["CreationType"] | TypeOfValue["SnapshotType"] | TypeOfValue["Type"] | Snapshot or instance | **Returns:** *O* The same object cast as an instance ___ ### castFlowReturn ▸ **castFlowReturn**<**T**>(`val`: T): *T* *Defined in [src/core/flow.ts:34](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/flow.ts#L34)* **`deprecated`** Not needed since TS3.6. Used for TypeScript to make flows that return a promise return the actual promise result. **Type parameters:** ▪ **T** **Parameters:** Name | Type | ------ | ------ | `val` | T | **Returns:** *T* ___ ### castToReferenceSnapshot ▸ **castToReferenceSnapshot**<**I**>(`instance`: I): *Extract extends never ? I : ReferenceIdentifier* *Defined in [src/core/mst-operations.ts:1011](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L1011)* Casts a node instance type to a reference snapshot type so it can be assigned to a reference snapshot (e.g. to be used inside a create call). Note that this is just a cast for the type system, this is, it won't actually convert an instance to a reference snapshot, but just fool typescript into thinking so. Example: ```ts const ModelA = types.model({ id: types.identifier, n: types.number }).actions(self => ({ setN(aNumber: number) { self.n = aNumber } })) const ModelB = types.model({ refA: types.reference(ModelA) }) const a = ModelA.create({ id: 'someId', n: 5 }); // this will allow the compiler to use a model as if it were a reference snapshot const b = ModelB.create({ refA: castToReferenceSnapshot(a)}) ``` **Type parameters:** ▪ **I** **Parameters:** Name | Type | Description | ------ | ------ | ------ | `instance` | I | Instance | **Returns:** *Extract extends never ? I : ReferenceIdentifier* The same object cast as a reference snapshot (string or number) ___ ### castToSnapshot ▸ **castToSnapshot**<**I**>(`snapshotOrInstance`: I): *Extract extends never ? I : TypeOfValue["CreationType"]* *Defined in [src/core/mst-operations.ts:977](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L977)* Casts a node instance type to a snapshot type so it can be assigned to a type snapshot (e.g. to be used inside a create call). Note that this is just a cast for the type system, this is, it won't actually convert an instance to a snapshot, but just fool typescript into thinking so. Example: ```ts const ModelA = types.model({ n: types.number }).actions(self => ({ setN(aNumber: number) { self.n = aNumber } })) const ModelB = types.model({ innerModel: ModelA }) const a = ModelA.create({ n: 5 }); // this will allow the compiler to use a model as if it were a snapshot const b = ModelB.create({ innerModel: castToSnapshot(a)}) ``` **Type parameters:** ▪ **I** **Parameters:** Name | Type | Description | ------ | ------ | ------ | `snapshotOrInstance` | I | Snapshot or instance | **Returns:** *Extract extends never ? I : TypeOfValue["CreationType"]* The same object cast as an input (creation) snapshot ___ ### clone ▸ **clone**<**T**>(`source`: T, `keepEnvironment`: boolean | any): *T* *Defined in [src/core/mst-operations.ts:666](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L666)* Returns a deep copy of the given state tree node as new tree. Shorthand for `snapshot(x) = getType(x).create(getSnapshot(x))` _Tip: clone will create a literal copy, including the same identifiers. To modify identifiers etc. during cloning, don't use clone but take a snapshot of the tree, modify it, and create new instance_ **Type parameters:** ▪ **T**: *IAnyStateTreeNode* **Parameters:** Name | Type | Default | Description | ------ | ------ | ------ | ------ | `source` | T | - | - | `keepEnvironment` | boolean | any | true | indicates whether the clone should inherit the same environment (`true`, the default), or not have an environment (`false`). If an object is passed in as second argument, that will act as the environment for the cloned tree. | **Returns:** *T* ___ ### compose ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›): *[IModelType](interfaces/imodeltype.md)‹PA & PB, OA & OB, _CustomJoin‹FCA, FCB›, _CustomJoin‹FSA, FSB››* *Defined in [src/types/complex-types/model.ts:812](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L812)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB, OA & OB, _CustomJoin‹FCA, FCB›, _CustomJoin‹FSA, FSB››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›): *[IModelType](interfaces/imodeltype.md)‹PA & PB, OA & OB, _CustomJoin‹FCA, FCB›, _CustomJoin‹FSA, FSB››* *Defined in [src/types/complex-types/model.ts:814](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L814)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB, OA & OB, _CustomJoin‹FCA, FCB›, _CustomJoin‹FSA, FSB››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC, OA & OB & OC, _CustomJoin‹FCA, _CustomJoin‹FCB, FCC››, _CustomJoin‹FSA, _CustomJoin‹FSB, FSC›››* *Defined in [src/types/complex-types/model.ts:816](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L816)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC, OA & OB & OC, _CustomJoin‹FCA, _CustomJoin‹FCB, FCC››, _CustomJoin‹FSA, _CustomJoin‹FSB, FSC›››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC, OA & OB & OC, _CustomJoin‹FCA, _CustomJoin‹FCB, FCC››, _CustomJoin‹FSA, _CustomJoin‹FSB, FSC›››* *Defined in [src/types/complex-types/model.ts:818](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L818)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC, OA & OB & OC, _CustomJoin‹FCA, _CustomJoin‹FCB, FCC››, _CustomJoin‹FSA, _CustomJoin‹FSB, FSC›››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD, OA & OB & OC & OD, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, FCD›››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, FSD››››* *Defined in [src/types/complex-types/model.ts:820](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L820)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD, OA & OB & OC & OD, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, FCD›››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, FSD››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD, OA & OB & OC & OD, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, FCD›››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, FSD››››* *Defined in [src/types/complex-types/model.ts:822](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L822)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD, OA & OB & OC & OD, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, FCD›››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, FSD››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE, OA & OB & OC & OD & OE, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, FCE››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, FSE›››››* *Defined in [src/types/complex-types/model.ts:824](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L824)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE, OA & OB & OC & OD & OE, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, FCE››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, FSE›››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE, OA & OB & OC & OD & OE, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, FCE››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, FSE›››››* *Defined in [src/types/complex-types/model.ts:826](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L826)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE, OA & OB & OC & OD & OE, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, FCE››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, FSE›››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF, OA & OB & OC & OD & OE & OF, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, FCF›››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, FSF››››››* *Defined in [src/types/complex-types/model.ts:830](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L830)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF, OA & OB & OC & OD & OE & OF, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, FCF›››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, FSF››››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF, OA & OB & OC & OD & OE & OF, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, FCF›››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, FSF››››››* *Defined in [src/types/complex-types/model.ts:833](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L833)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF, OA & OB & OC & OD & OE & OF, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, FCF›››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, FSF››››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**, **PG**, **OG**, **FCG**, **FSG**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›, `G`: [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG, OA & OB & OC & OD & OE & OF & OG, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, FCG››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, FSG›››››››* *Defined in [src/types/complex-types/model.ts:836](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L836)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** ▪ **PG**: *ModelProperties* ▪ **OG** ▪ **FCG** ▪ **FSG** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | `G` | [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG, OA & OB & OC & OD & OE & OF & OG, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, FCG››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, FSG›››››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**, **PG**, **OG**, **FCG**, **FSG**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›, `G`: [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG, OA & OB & OC & OD & OE & OF & OG, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, FCG››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, FSG›››››››* *Defined in [src/types/complex-types/model.ts:839](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L839)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** ▪ **PG**: *ModelProperties* ▪ **OG** ▪ **FCG** ▪ **FSG** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | `G` | [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG, OA & OB & OC & OD & OE & OF & OG, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, FCG››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, FSG›››››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**, **PG**, **OG**, **FCG**, **FSG**, **PH**, **OH**, **FCH**, **FSH**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›, `G`: [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG›, `H`: [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH, OA & OB & OC & OD & OE & OF & OG & OH, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, FCH›››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, FSH››››››››* *Defined in [src/types/complex-types/model.ts:842](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L842)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** ▪ **PG**: *ModelProperties* ▪ **OG** ▪ **FCG** ▪ **FSG** ▪ **PH**: *ModelProperties* ▪ **OH** ▪ **FCH** ▪ **FSH** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | `G` | [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG› | `H` | [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH, OA & OB & OC & OD & OE & OF & OG & OH, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, FCH›››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, FSH››››››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**, **PG**, **OG**, **FCG**, **FSG**, **PH**, **OH**, **FCH**, **FSH**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›, `G`: [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG›, `H`: [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH, OA & OB & OC & OD & OE & OF & OG & OH, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, FCH›››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, FSH››››››››* *Defined in [src/types/complex-types/model.ts:845](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L845)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** ▪ **PG**: *ModelProperties* ▪ **OG** ▪ **FCG** ▪ **FSG** ▪ **PH**: *ModelProperties* ▪ **OH** ▪ **FCH** ▪ **FSH** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | `G` | [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG› | `H` | [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH, OA & OB & OC & OD & OE & OF & OG & OH, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, FCH›››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, FSH››››››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**, **PG**, **OG**, **FCG**, **FSG**, **PH**, **OH**, **FCH**, **FSH**, **PI**, **OI**, **FCI**, **FSI**>(`name`: string, `A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›, `G`: [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG›, `H`: [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH›, `I`: [IModelType](interfaces/imodeltype.md)‹PI, OI, FCI, FSI›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH & PI, OA & OB & OC & OD & OE & OF & OG & OH & OI, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, _CustomJoin‹FCH, FCI››››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, _CustomJoin‹FSH, FSI›››››››››* *Defined in [src/types/complex-types/model.ts:848](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L848)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** ▪ **PG**: *ModelProperties* ▪ **OG** ▪ **FCG** ▪ **FSG** ▪ **PH**: *ModelProperties* ▪ **OH** ▪ **FCH** ▪ **FSH** ▪ **PI**: *ModelProperties* ▪ **OI** ▪ **FCI** ▪ **FSI** **Parameters:** Name | Type | ------ | ------ | `name` | string | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | `G` | [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG› | `H` | [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH› | `I` | [IModelType](interfaces/imodeltype.md)‹PI, OI, FCI, FSI› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH & PI, OA & OB & OC & OD & OE & OF & OG & OH & OI, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, _CustomJoin‹FCH, FCI››››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, _CustomJoin‹FSH, FSI›››››››››* ▸ **compose**<**PA**, **OA**, **FCA**, **FSA**, **PB**, **OB**, **FCB**, **FSB**, **PC**, **OC**, **FCC**, **FSC**, **PD**, **OD**, **FCD**, **FSD**, **PE**, **OE**, **FCE**, **FSE**, **PF**, **OF**, **FCF**, **FSF**, **PG**, **OG**, **FCG**, **FSG**, **PH**, **OH**, **FCH**, **FSH**, **PI**, **OI**, **FCI**, **FSI**>(`A`: [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA›, `B`: [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB›, `C`: [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC›, `D`: [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD›, `E`: [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE›, `F`: [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF›, `G`: [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG›, `H`: [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH›, `I`: [IModelType](interfaces/imodeltype.md)‹PI, OI, FCI, FSI›): *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH & PI, OA & OB & OC & OD & OE & OF & OG & OH & OI, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, _CustomJoin‹FCH, FCI››››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, _CustomJoin‹FSH, FSI›››››››››* *Defined in [src/types/complex-types/model.ts:851](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L851)* `types.compose` - Composes a new model from one or more existing model types. This method can be invoked in two forms: Given 2 or more model types, the types are composed into a new Type. Given first parameter as a string and 2 or more model types, the types are composed into a new Type with the given name **Type parameters:** ▪ **PA**: *ModelProperties* ▪ **OA** ▪ **FCA** ▪ **FSA** ▪ **PB**: *ModelProperties* ▪ **OB** ▪ **FCB** ▪ **FSB** ▪ **PC**: *ModelProperties* ▪ **OC** ▪ **FCC** ▪ **FSC** ▪ **PD**: *ModelProperties* ▪ **OD** ▪ **FCD** ▪ **FSD** ▪ **PE**: *ModelProperties* ▪ **OE** ▪ **FCE** ▪ **FSE** ▪ **PF**: *ModelProperties* ▪ **OF** ▪ **FCF** ▪ **FSF** ▪ **PG**: *ModelProperties* ▪ **OG** ▪ **FCG** ▪ **FSG** ▪ **PH**: *ModelProperties* ▪ **OH** ▪ **FCH** ▪ **FSH** ▪ **PI**: *ModelProperties* ▪ **OI** ▪ **FCI** ▪ **FSI** **Parameters:** Name | Type | ------ | ------ | `A` | [IModelType](interfaces/imodeltype.md)‹PA, OA, FCA, FSA› | `B` | [IModelType](interfaces/imodeltype.md)‹PB, OB, FCB, FSB› | `C` | [IModelType](interfaces/imodeltype.md)‹PC, OC, FCC, FSC› | `D` | [IModelType](interfaces/imodeltype.md)‹PD, OD, FCD, FSD› | `E` | [IModelType](interfaces/imodeltype.md)‹PE, OE, FCE, FSE› | `F` | [IModelType](interfaces/imodeltype.md)‹PF, OF, FCF, FSF› | `G` | [IModelType](interfaces/imodeltype.md)‹PG, OG, FCG, FSG› | `H` | [IModelType](interfaces/imodeltype.md)‹PH, OH, FCH, FSH› | `I` | [IModelType](interfaces/imodeltype.md)‹PI, OI, FCI, FSI› | **Returns:** *[IModelType](interfaces/imodeltype.md)‹PA & PB & PC & PD & PE & PF & PG & PH & PI, OA & OB & OC & OD & OE & OF & OG & OH & OI, _CustomJoin‹FCA, _CustomJoin‹FCB, _CustomJoin‹FCC, _CustomJoin‹FCD, _CustomJoin‹FCE, _CustomJoin‹FCF, _CustomJoin‹FCG, _CustomJoin‹FCH, FCI››››››››, _CustomJoin‹FSA, _CustomJoin‹FSB, _CustomJoin‹FSC, _CustomJoin‹FSD, _CustomJoin‹FSE, _CustomJoin‹FSF, _CustomJoin‹FSG, _CustomJoin‹FSH, FSI›››››››››* ___ ### createActionTrackingMiddleware ▸ **createActionTrackingMiddleware**<**T**>(`hooks`: [IActionTrackingMiddlewareHooks](interfaces/iactiontrackingmiddlewarehooks.md)‹T›): *[IMiddlewareHandler](index.md#imiddlewarehandler)* *Defined in [src/middlewares/create-action-tracking-middleware.ts:28](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/create-action-tracking-middleware.ts#L28)* Note: Consider migrating to `createActionTrackingMiddleware2`, it is easier to use. Convenience utility to create action based middleware that supports async processes more easily. All hooks are called for both synchronous and asynchronous actions. Except that either `onSuccess` or `onFail` is called The create middleware tracks the process of an action (assuming it passes the `filter`). `onResume` can return any value, which will be passed as second argument to any other hook. This makes it possible to keep state during a process. See the `atomic` middleware for an example **Type parameters:** ▪ **T** **Parameters:** Name | Type | ------ | ------ | `hooks` | [IActionTrackingMiddlewareHooks](interfaces/iactiontrackingmiddlewarehooks.md)‹T› | **Returns:** *[IMiddlewareHandler](index.md#imiddlewarehandler)* ___ ### createActionTrackingMiddleware2 ▸ **createActionTrackingMiddleware2**<**TEnv**>(`middlewareHooks`: [IActionTrackingMiddleware2Hooks](interfaces/iactiontrackingmiddleware2hooks.md)‹TEnv›): *[IMiddlewareHandler](index.md#imiddlewarehandler)* *Defined in [src/middlewares/createActionTrackingMiddleware2.ts:72](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/createActionTrackingMiddleware2.ts#L72)* Convenience utility to create action based middleware that supports async processes more easily. The flow is like this: - for each action: if filter passes -> `onStart` -> (inner actions recursively) -> `onFinish` Example: if we had an action `a` that called inside an action `b1`, then `b2` the flow would be: - `filter(a)` - `onStart(a)` - `filter(b1)` - `onStart(b1)` - `onFinish(b1)` - `filter(b2)` - `onStart(b2)` - `onFinish(b2)` - `onFinish(a)` The flow is the same no matter if the actions are sync or async. See the `atomic` middleware for an example **Type parameters:** ▪ **TEnv** **Parameters:** Name | Type | ------ | ------ | `middlewareHooks` | [IActionTrackingMiddleware2Hooks](interfaces/iactiontrackingmiddleware2hooks.md)‹TEnv› | **Returns:** *[IMiddlewareHandler](index.md#imiddlewarehandler)* ___ ### custom ▸ **custom**<**S**, **T**>(`options`: [CustomTypeOptions](interfaces/customtypeoptions.md)‹S, T›): *[IType](interfaces/itype.md)‹S | T, S, T›* *Defined in [src/types/utility-types/custom.ts:74](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/custom.ts#L74)* `types.custom` - Creates a custom type. Custom types can be used for arbitrary immutable values, that have a serializable representation. For example, to create your own Date representation, Decimal type etc. The signature of the options is: ```ts export interface CustomTypeOptions { // Friendly name name: string // given a serialized value and environment, how to turn it into the target type fromSnapshot(snapshot: S, env: any): T // return the serialization of the current value toSnapshot(value: T): S // if true, this is a converted value, if false, it's a snapshot isTargetType(value: T | S): value is T // a non empty string is assumed to be a validation error getValidationMessage?(snapshot: S): string } ``` Example: ```ts const DecimalPrimitive = types.custom({ name: "Decimal", fromSnapshot(value: string) { return new Decimal(value) }, toSnapshot(value: Decimal) { return value.toString() }, isTargetType(value: string | Decimal): boolean { return value instanceof Decimal }, getValidationMessage(value: string): string { if (/^-?\d+\.\d+$/.test(value)) return "" // OK return `'${value}' doesn't look like a valid decimal number` } }) const Wallet = types.model({ balance: DecimalPrimitive }) ``` **Type parameters:** ▪ **S** ▪ **T** **Parameters:** Name | Type | ------ | ------ | `options` | [CustomTypeOptions](interfaces/customtypeoptions.md)‹S, T› | **Returns:** *[IType](interfaces/itype.md)‹S | T, S, T›* ___ ### decorate ▸ **decorate**<**T**>(`handler`: [IMiddlewareHandler](index.md#imiddlewarehandler), `fn`: T, `includeHooks`: boolean): *T* *Defined in [src/core/action.ts:213](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L213)* Binds middleware to a specific action. Example: ```ts type.actions(self => { function takeA____() { self.toilet.donate() self.wipe() self.wipe() self.toilet.flush() } return { takeA____: decorate(atomic, takeA____) } }) ``` **Type parameters:** ▪ **T**: *Function* **Parameters:** Name | Type | Default | ------ | ------ | ------ | `handler` | [IMiddlewareHandler](index.md#imiddlewarehandler) | - | `fn` | T | - | `includeHooks` | boolean | true | **Returns:** *T* The original function ___ ### destroy ▸ **destroy**(`target`: IAnyStateTreeNode): *void* *Defined in [src/core/mst-operations.ts:698](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L698)* Removes a model element from the state tree, and mark it as end-of-life; the element should not be used anymore **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *void* ___ ### detach ▸ **detach**<**T**>(`target`: T): *T* *Defined in [src/core/mst-operations.ts:687](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L687)* Removes a model element from the state tree, and let it live on as a new state tree **Type parameters:** ▪ **T**: *IAnyStateTreeNode* **Parameters:** Name | Type | ------ | ------ | `target` | T | **Returns:** *T* ___ ### enumeration ▸ **enumeration**<**T**>(`options`: keyof T[]): *[ISimpleType](interfaces/isimpletype.md)‹UnionStringArray‹T[]››* *Defined in [src/types/utility-types/enumeration.ts:11](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/enumeration.ts#L11)* `types.enumeration` - Can be used to create an string based enumeration. (note: this methods is just sugar for a union of string literals) Example: ```ts const TrafficLight = types.model({ color: types.enumeration("Color", ["Red", "Orange", "Green"]) }) ``` **Type parameters:** ▪ **T**: *string* **Parameters:** Name | Type | Description | ------ | ------ | ------ | `options` | keyof T[] | possible values this enumeration can have | **Returns:** *[ISimpleType](interfaces/isimpletype.md)‹UnionStringArray‹T[]››* ▸ **enumeration**<**T**>(`name`: string, `options`: keyof T[]): *[ISimpleType](interfaces/isimpletype.md)‹UnionStringArray‹T[]››* *Defined in [src/types/utility-types/enumeration.ts:14](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/enumeration.ts#L14)* `types.enumeration` - Can be used to create an string based enumeration. (note: this methods is just sugar for a union of string literals) Example: ```ts const TrafficLight = types.model({ color: types.enumeration("Color", ["Red", "Orange", "Green"]) }) ``` **Type parameters:** ▪ **T**: *string* **Parameters:** Name | Type | Description | ------ | ------ | ------ | `name` | string | descriptive name of the enumeration (optional) | `options` | keyof T[] | possible values this enumeration can have | **Returns:** *[ISimpleType](interfaces/isimpletype.md)‹UnionStringArray‹T[]››* ___ ### escapeJsonPath ▸ **escapeJsonPath**(`path`: string): *string* *Defined in [src/core/json-patch.ts:78](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L78)* Escape slashes and backslashes. http://tools.ietf.org/html/rfc6901 **Parameters:** Name | Type | ------ | ------ | `path` | string | **Returns:** *string* ___ ### flow ▸ **flow**<**R**, **Args**>(`generator`: function): *function* *Defined in [src/core/flow.ts:21](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/flow.ts#L21)* See [asynchronous actions](concepts/async-actions.md). **Type parameters:** ▪ **R** ▪ **Args**: *any[]* **Parameters:** ▪ **generator**: *function* ▸ (...`args`: Args): *Generator‹PromiseLike‹any›, R, any›* **Parameters:** Name | Type | ------ | ------ | `...args` | Args | **Returns:** *function* The flow as a promise. ▸ (...`args`: Args): *Promise‹FlowReturn‹R››* **Parameters:** Name | Type | ------ | ------ | `...args` | Args | ___ ### frozen ▸ **frozen**<**C**>(`subType`: [IType](interfaces/itype.md)‹C, any, any›): *[IType](interfaces/itype.md)‹C, C, C›* *Defined in [src/types/utility-types/frozen.ts:59](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/frozen.ts#L59)* `types.frozen` - Frozen can be used to store any value that is serializable in itself (that is valid JSON). Frozen values need to be immutable or treated as if immutable. They need be serializable as well. Values stored in frozen will snapshotted as-is by MST, and internal changes will not be tracked. This is useful to store complex, but immutable values like vectors etc. It can form a powerful bridge to parts of your application that should be immutable, or that assume data to be immutable. Note: if you want to store free-form state that is mutable, or not serializeable, consider using volatile state instead. Frozen properties can be defined in three different ways 1. `types.frozen(SubType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type 2. `types.frozen({ someDefaultValue: true})` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field 3. `types.frozen()` - provide a typescript type, to help in strongly typing the field (design time only) Example: ```ts const GameCharacter = types.model({ name: string, location: types.frozen({ x: 0, y: 0}) }) const hero = GameCharacter.create({ name: "Mario", location: { x: 7, y: 4 } }) hero.location = { x: 10, y: 2 } // OK hero.location.x = 7 // Not ok! ``` ```ts type Point = { x: number, y: number } const Mouse = types.model({ loc: types.frozen() }) ``` **Type parameters:** ▪ **C** **Parameters:** Name | Type | ------ | ------ | `subType` | [IType](interfaces/itype.md)‹C, any, any› | **Returns:** *[IType](interfaces/itype.md)‹C, C, C›* ▸ **frozen**<**T**>(`defaultValue`: T): *[IType](interfaces/itype.md)‹T | undefined | null, T, T›* *Defined in [src/types/utility-types/frozen.ts:60](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/frozen.ts#L60)* `types.frozen` - Frozen can be used to store any value that is serializable in itself (that is valid JSON). Frozen values need to be immutable or treated as if immutable. They need be serializable as well. Values stored in frozen will snapshotted as-is by MST, and internal changes will not be tracked. This is useful to store complex, but immutable values like vectors etc. It can form a powerful bridge to parts of your application that should be immutable, or that assume data to be immutable. Note: if you want to store free-form state that is mutable, or not serializeable, consider using volatile state instead. Frozen properties can be defined in three different ways 1. `types.frozen(SubType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type 2. `types.frozen({ someDefaultValue: true})` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field 3. `types.frozen()` - provide a typescript type, to help in strongly typing the field (design time only) Example: ```ts const GameCharacter = types.model({ name: string, location: types.frozen({ x: 0, y: 0}) }) const hero = GameCharacter.create({ name: "Mario", location: { x: 7, y: 4 } }) hero.location = { x: 10, y: 2 } // OK hero.location.x = 7 // Not ok! ``` ```ts type Point = { x: number, y: number } const Mouse = types.model({ loc: types.frozen() }) ``` **Type parameters:** ▪ **T** **Parameters:** Name | Type | ------ | ------ | `defaultValue` | T | **Returns:** *[IType](interfaces/itype.md)‹T | undefined | null, T, T›* ▸ **frozen**<**T**>(): *[IType](interfaces/itype.md)‹T, T, T›* *Defined in [src/types/utility-types/frozen.ts:61](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/frozen.ts#L61)* `types.frozen` - Frozen can be used to store any value that is serializable in itself (that is valid JSON). Frozen values need to be immutable or treated as if immutable. They need be serializable as well. Values stored in frozen will snapshotted as-is by MST, and internal changes will not be tracked. This is useful to store complex, but immutable values like vectors etc. It can form a powerful bridge to parts of your application that should be immutable, or that assume data to be immutable. Note: if you want to store free-form state that is mutable, or not serializeable, consider using volatile state instead. Frozen properties can be defined in three different ways 1. `types.frozen(SubType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type 2. `types.frozen({ someDefaultValue: true})` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field 3. `types.frozen()` - provide a typescript type, to help in strongly typing the field (design time only) Example: ```ts const GameCharacter = types.model({ name: string, location: types.frozen({ x: 0, y: 0}) }) const hero = GameCharacter.create({ name: "Mario", location: { x: 7, y: 4 } }) hero.location = { x: 10, y: 2 } // OK hero.location.x = 7 // Not ok! ``` ```ts type Point = { x: number, y: number } const Mouse = types.model({ loc: types.frozen() }) ``` **Type parameters:** ▪ **T** **Returns:** *[IType](interfaces/itype.md)‹T, T, T›* ___ ### getChildType ▸ **getChildType**(`object`: IAnyStateTreeNode, `propertyName?`: undefined | string): *[IAnyType](interfaces/ianytype.md)* *Defined in [src/core/mst-operations.ts:68](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L68)* Returns the _declared_ type of the given sub property of an object, array or map. In the case of arrays and maps the property name is optional and will be ignored. Example: ```ts const Box = types.model({ x: 0, y: 0 }) const box = Box.create() console.log(getChildType(box, "x").name) // 'number' ``` **Parameters:** Name | Type | ------ | ------ | `object` | IAnyStateTreeNode | `propertyName?` | undefined | string | **Returns:** *[IAnyType](interfaces/ianytype.md)* ___ ### getEnv ▸ **getEnv**<**T**>(`target`: IAnyStateTreeNode): *T* *Defined in [src/core/mst-operations.ts:773](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L773)* Returns the environment of the current state tree, or throws. For more info on environments, see [Dependency injection](/concepts/dependency-injection) Please note that in child nodes access to the root is only possible once the `afterAttach` hook has fired Returns an empty environment if the tree wasn't initialized with an environment **Type parameters:** ▪ **T** **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *T* ___ ### getIdentifier ▸ **getIdentifier**(`target`: IAnyStateTreeNode): *string | null* *Defined in [src/core/mst-operations.ts:549](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L549)* Returns the identifier of the target node. This is the *string normalized* identifier, which might not match the type of the identifier attribute **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *string | null* ___ ### getLivelinessChecking ▸ **getLivelinessChecking**(): *[LivelinessMode](index.md#livelinessmode)* *Defined in [src/core/node/livelinessChecking.ts:27](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/livelinessChecking.ts#L27)* Returns the current liveliness checking mode. **Returns:** *[LivelinessMode](index.md#livelinessmode)* `"warn"`, `"error"` or `"ignore"` ___ ### getMembers ▸ **getMembers**(`target`: IAnyStateTreeNode): *[IModelReflectionData](interfaces/imodelreflectiondata.md)* *Defined in [src/core/mst-operations.ts:874](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L874)* Returns a reflection of the model node, including name, properties, views, volatile state, and actions. `flowActions` is also provided as a separate array of names for any action that came from a flow generator as well. In the case where a model has two actions: `doSomething` and `doSomethingWithFlow`, where `doSomethingWithFlow` is a flow generator, the `actions` array will contain both actions, i.e. ["doSomething", "doSomethingWithFlow"], and the `flowActions` array will contain only the flow action, i.e. ["doSomethingWithFlow"]. **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *[IModelReflectionData](interfaces/imodelreflectiondata.md)* ___ ### getNodeId ▸ **getNodeId**(`target`: IAnyStateTreeNode): *number* *Defined in [src/core/mst-operations.ts:1026](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L1026)* Returns the unique node id (not to be confused with the instance identifier) for a given instance. This id is a number that is unique for each instance. **`export`** **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *number* ___ ### getParent ▸ **getParent**<**IT**>(`target`: IAnyStateTreeNode, `depth`: number): *TypeOrStateTreeNodeToStateTreeNode‹IT›* *Defined in [src/core/mst-operations.ts:382](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L382)* Returns the immediate parent of this object, or throws. Note that the immediate parent can be either an object, map or array, and doesn't necessarily refer to the parent model. Please note that in child nodes access to the root is only possible once the `afterAttach` hook has fired. **Type parameters:** ▪ **IT**: *IAnyStateTreeNode | [IAnyComplexType](interfaces/ianycomplextype.md)* **Parameters:** Name | Type | Default | Description | ------ | ------ | ------ | ------ | `target` | IAnyStateTreeNode | - | - | `depth` | number | 1 | How far should we look upward? 1 by default. | **Returns:** *TypeOrStateTreeNodeToStateTreeNode‹IT›* ___ ### getParentOfType ▸ **getParentOfType**<**IT**>(`target`: IAnyStateTreeNode, `type`: IT): *IT["Type"]* *Defined in [src/core/mst-operations.ts:426](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L426)* Returns the target's parent of a given type, or throws. **Type parameters:** ▪ **IT**: *[IAnyComplexType](interfaces/ianycomplextype.md)* **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | `type` | IT | **Returns:** *IT["Type"]* ___ ### getPath ▸ **getPath**(`target`: IAnyStateTreeNode): *string* *Defined in [src/core/mst-operations.ts:466](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L466)* Returns the path of the given object in the model tree **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *string* ___ ### getPathParts ▸ **getPathParts**(`target`: IAnyStateTreeNode): *string[]* *Defined in [src/core/mst-operations.ts:479](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L479)* Returns the path of the given object as unescaped string array. **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *string[]* ___ ### getPropertyMembers ▸ **getPropertyMembers**(`typeOrNode`: [IAnyModelType](interfaces/ianymodeltype.md) | IAnyStateTreeNode): *[IModelReflectionPropertiesData](interfaces/imodelreflectionpropertiesdata.md)* *Defined in [src/core/mst-operations.ts:835](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L835)* Returns a reflection of the model type properties and name for either a model type or model node. **Parameters:** Name | Type | ------ | ------ | `typeOrNode` | [IAnyModelType](interfaces/ianymodeltype.md) | IAnyStateTreeNode | **Returns:** *[IModelReflectionPropertiesData](interfaces/imodelreflectionpropertiesdata.md)* ___ ### getRelativePath ▸ **getRelativePath**(`base`: IAnyStateTreeNode, `target`: IAnyStateTreeNode): *string* *Defined in [src/core/mst-operations.ts:648](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L648)* Given two state tree nodes that are part of the same tree, returns the shortest jsonpath needed to navigate from the one to the other **Parameters:** Name | Type | ------ | ------ | `base` | IAnyStateTreeNode | `target` | IAnyStateTreeNode | **Returns:** *string* ___ ### getRoot ▸ **getRoot**<**IT**>(`target`: IAnyStateTreeNode): *TypeOrStateTreeNodeToStateTreeNode‹IT›* *Defined in [src/core/mst-operations.ts:451](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L451)* Given an object in a model tree, returns the root object of that tree. Please note that in child nodes access to the root is only possible once the `afterAttach` hook has fired. **Type parameters:** ▪ **IT**: *[IAnyComplexType](interfaces/ianycomplextype.md) | IAnyStateTreeNode* **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *TypeOrStateTreeNodeToStateTreeNode‹IT›* ___ ### getRunningActionContext ▸ **getRunningActionContext**(): *[IActionContext](interfaces/iactioncontext.md) | undefined* *Defined in [src/core/actionContext.ts:26](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L26)* Returns the currently executing MST action context, or undefined if none. **Returns:** *[IActionContext](interfaces/iactioncontext.md) | undefined* ___ ### getSnapshot ▸ **getSnapshot**<**S**>(`target`: IStateTreeNode‹[IType](interfaces/itype.md)‹any, S, any››, `applyPostProcess`: boolean): *S* *Defined in [src/core/mst-operations.ts:336](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L336)* Calculates a snapshot from the given model instance. The snapshot will always reflect the latest state but use structural sharing where possible. Doesn't require MobX transactions to be completed. **Type parameters:** ▪ **S** **Parameters:** Name | Type | Default | Description | ------ | ------ | ------ | ------ | `target` | IStateTreeNode‹[IType](interfaces/itype.md)‹any, S, any›› | - | - | `applyPostProcess` | boolean | true | If true (the default) then postProcessSnapshot gets applied. | **Returns:** *S* ___ ### getType ▸ **getType**(`object`: IAnyStateTreeNode): *[IAnyComplexType](interfaces/ianycomplextype.md)* *Defined in [src/core/mst-operations.ts:46](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L46)* Returns the _actual_ type of the given tree node. (Or throws) **Parameters:** Name | Type | ------ | ------ | `object` | IAnyStateTreeNode | **Returns:** *[IAnyComplexType](interfaces/ianycomplextype.md)* ___ ### hasEnv ▸ **hasEnv**(`target`: IAnyStateTreeNode): *boolean* *Defined in [src/core/mst-operations.ts:790](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L790)* Returns whether the current state tree has environment or not. **`export`** **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *boolean* ___ ### hasParent ▸ **hasParent**(`target`: IAnyStateTreeNode, `depth`: number): *boolean* *Defined in [src/core/mst-operations.ts:356](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L356)* Given a model instance, returns `true` if the object has a parent, that is, is part of another object, map or array. **Parameters:** Name | Type | Default | Description | ------ | ------ | ------ | ------ | `target` | IAnyStateTreeNode | - | - | `depth` | number | 1 | How far should we look upward? 1 by default. | **Returns:** *boolean* ___ ### hasParentOfType ▸ **hasParentOfType**(`target`: IAnyStateTreeNode, `type`: [IAnyComplexType](interfaces/ianycomplextype.md)): *boolean* *Defined in [src/core/mst-operations.ts:406](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L406)* Given a model instance, returns `true` if the object has a parent of given type, that is, is part of another object, map or array **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | `type` | [IAnyComplexType](interfaces/ianycomplextype.md) | **Returns:** *boolean* ___ ### isActionContextChildOf ▸ **isActionContextChildOf**(`actionContext`: [IActionContext](interfaces/iactioncontext.md), `parent`: number | [IActionContext](interfaces/iactioncontext.md) | [IMiddlewareEvent](interfaces/imiddlewareevent.md)): *boolean* *Defined in [src/core/actionContext.ts:56](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L56)* Returns if the given action context is a parent of this action context. **Parameters:** Name | Type | ------ | ------ | `actionContext` | [IActionContext](interfaces/iactioncontext.md) | `parent` | number | [IActionContext](interfaces/iactioncontext.md) | [IMiddlewareEvent](interfaces/imiddlewareevent.md) | **Returns:** *boolean* ___ ### isActionContextThisOrChildOf ▸ **isActionContextThisOrChildOf**(`actionContext`: [IActionContext](interfaces/iactioncontext.md), `parentOrThis`: number | [IActionContext](interfaces/iactioncontext.md) | [IMiddlewareEvent](interfaces/imiddlewareevent.md)): *boolean* *Defined in [src/core/actionContext.ts:66](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L66)* Returns if the given action context is this or a parent of this action context. **Parameters:** Name | Type | ------ | ------ | `actionContext` | [IActionContext](interfaces/iactioncontext.md) | `parentOrThis` | number | [IActionContext](interfaces/iactioncontext.md) | [IMiddlewareEvent](interfaces/imiddlewareevent.md) | **Returns:** *boolean* ___ ### isAlive ▸ **isAlive**(`target`: IAnyStateTreeNode): *boolean* *Defined in [src/core/mst-operations.ts:716](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L716)* Returns true if the given state tree node is not killed yet. This means that the node is still a part of a tree, and that `destroy` has not been called. If a node is not alive anymore, the only thing one can do with it is requesting it's last path and snapshot **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *boolean* ___ ### isArrayType ▸ **isArrayType**(`type`: unknown): *type is IArrayType* *Defined in [src/types/complex-types/array.ts:512](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/array.ts#L512)* Returns if a given value represents an array type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IArrayType* `true` if the type is an array type. ___ ### isFrozenType ▸ **isFrozenType**(`type`: unknown): *type is ISimpleType* *Defined in [src/types/utility-types/frozen.ts:114](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/frozen.ts#L114)* Returns if a given value represents a frozen type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is ISimpleType* ___ ### isIdentifierType ▸ **isIdentifierType**(`type`: unknown): *type is ISimpleType | ISimpleType* *Defined in [src/types/utility-types/identifier.ts:138](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/identifier.ts#L138)* Returns if a given value represents an identifier type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is ISimpleType | ISimpleType* ___ ### isLateType ▸ **isLateType**(`type`: unknown): *type is IAnyType* *Defined in [src/types/utility-types/late.ts:144](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/late.ts#L144)* Returns if a given value represents a late type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IAnyType* ___ ### isLiteralType ▸ **isLiteralType**(`type`: unknown): *type is ISimpleType* *Defined in [src/types/utility-types/literal.ts:85](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/literal.ts#L85)* Returns if a given value represents a literal type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is ISimpleType* ___ ### isMapType ▸ **isMapType**(`type`: unknown): *type is IMapType* *Defined in [src/types/complex-types/map.ts:521](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/map.ts#L521)* Returns if a given value represents a map type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IMapType* `true` if it is a map type. ___ ### isModelType ▸ **isModelType**(`type`: unknown): *type is IAnyModelType* *Defined in [src/types/complex-types/model.ts:897](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L897)* Returns if a given value represents a model type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IAnyModelType* ___ ### isOptionalType ▸ **isOptionalType**(`type`: unknown): *type is IOptionalIType* *Defined in [src/types/utility-types/optional.ts:234](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/optional.ts#L234)* Returns if a value represents an optional type. **`template`** IT **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IOptionalIType* ___ ### isPrimitiveType ▸ **isPrimitiveType**(`type`: unknown): *type is ISimpleType | ISimpleType | ISimpleType | IType* *Defined in [src/types/primitives.ts:241](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/primitives.ts#L241)* Returns if a given value represents a primitive type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is ISimpleType | ISimpleType | ISimpleType | IType* ___ ### isProtected ▸ **isProtected**(`target`: IAnyStateTreeNode): *boolean* *Defined in [src/core/mst-operations.ts:310](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L310)* Returns true if the object is in protected mode, @see protect **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *boolean* ___ ### isReferenceType ▸ **isReferenceType**(`type`: unknown): *type is IReferenceType* *Defined in [src/types/utility-types/reference.ts:541](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L541)* Returns if a given value represents a reference type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IReferenceType* ___ ### isRefinementType ▸ **isRefinementType**(`type`: unknown): *type is IAnyType* *Defined in [src/types/utility-types/refinement.ts:125](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/refinement.ts#L125)* Returns if a given value is a refinement type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IAnyType* ___ ### isRoot ▸ **isRoot**(`target`: IAnyStateTreeNode): *boolean* *Defined in [src/core/mst-operations.ts:492](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L492)* Returns true if the given object is the root of a model tree. **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *boolean* ___ ### isStateTreeNode ▸ **isStateTreeNode**<**IT**>(`value`: any): *value is STNValue, IT>* *Defined in [src/core/node/node-utils.ts:67](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/node-utils.ts#L67)* Returns true if the given value is a node in a state tree. More precisely, that is, if the value is an instance of a `types.model`, `types.array` or `types.map`. **Type parameters:** ▪ **IT**: *[IAnyComplexType](interfaces/ianycomplextype.md)* **Parameters:** Name | Type | ------ | ------ | `value` | any | **Returns:** *value is STNValue, IT>* true if the value is a state tree node. ___ ### isType ▸ **isType**(`value`: any): *value is IAnyType* *Defined in [src/core/type/type.ts:541](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L541)* Returns if a given value represents a type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `value` | any | Value to check. | **Returns:** *value is IAnyType* `true` if the value is a type. ___ ### isUnionType ▸ **isUnionType**(`type`: unknown): *type is IUnionType* *Defined in [src/types/utility-types/union.ts:218](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L218)* Returns if a given value represents a union type. **Parameters:** Name | Type | ------ | ------ | `type` | unknown | **Returns:** *type is IUnionType* ___ ### isValidReference ▸ **isValidReference**<**N**>(`getter`: function, `checkIfAlive`: boolean): *boolean* *Defined in [src/core/mst-operations.ts:596](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L596)* Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns if the check passes or not. **Type parameters:** ▪ **N**: *IAnyStateTreeNode* **Parameters:** ▪ **getter**: *function* Function to access the reference. ▸ (): *N | null | undefined* ▪`Default value` **checkIfAlive**: *boolean*= true true to also make sure the referenced node is alive (default), false to skip this check. **Returns:** *boolean* ___ ### joinJsonPath ▸ **joinJsonPath**(`path`: string[]): *string* *Defined in [src/core/json-patch.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L99)* Generates a json-path compliant json path from path parts. **Parameters:** Name | Type | ------ | ------ | `path` | string[] | **Returns:** *string* ___ ### late ▸ **late**<**T**>(`type`: function): *T* *Defined in [src/types/utility-types/late.ts:106](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/late.ts#L106)* `types.late` - Defines a type that gets implemented later. This is useful when you have to deal with circular dependencies. Please notice that when defining circular dependencies TypeScript isn't smart enough to inference them. Example: ```ts // TypeScript isn't smart enough to infer self referencing types. const Node = types.model({ children: types.array(types.late((): IAnyModelType => Node)) // then typecast each array element to Instance }) ``` **Type parameters:** ▪ **T**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** ▪ **type**: *function* A function that returns the type that will be defined. ▸ (): *T* **Returns:** *T* ▸ **late**<**T**>(`name`: string, `type`: function): *T* *Defined in [src/types/utility-types/late.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/late.ts#L107)* `types.late` - Defines a type that gets implemented later. This is useful when you have to deal with circular dependencies. Please notice that when defining circular dependencies TypeScript isn't smart enough to inference them. Example: ```ts // TypeScript isn't smart enough to infer self referencing types. const Node = types.model({ children: types.array(types.late((): IAnyModelType => Node)) // then typecast each array element to Instance }) ``` **Type parameters:** ▪ **T**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** ▪ **name**: *string* The name to use for the type that will be returned. ▪ **type**: *function* A function that returns the type that will be defined. ▸ (): *T* **Returns:** *T* ___ ### lazy ▸ **lazy**<**T**, **U**>(`name`: string, `options`: LazyOptions‹T, U›): *T* *Defined in [src/types/utility-types/lazy.ts:22](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/lazy.ts#L22)* **Type parameters:** ▪ **T**: *[IType](interfaces/itype.md)‹any, any, any›* ▪ **U** **Parameters:** Name | Type | ------ | ------ | `name` | string | `options` | LazyOptions‹T, U› | **Returns:** *T* ___ ### literal ▸ **literal**<**S**>(`value`: S): *[ISimpleType](interfaces/isimpletype.md)‹S›* *Defined in [src/types/utility-types/literal.ts:72](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/literal.ts#L72)* `types.literal` - The literal type will return a type that will match only the exact given type. The given value must be a primitive, in order to be serialized to a snapshot correctly. You can use literal to match exact strings for example the exact male or female string. Example: ```ts const Person = types.model({ name: types.string, gender: types.union(types.literal('male'), types.literal('female')) }) ``` **Type parameters:** ▪ **S**: *Primitives* **Parameters:** Name | Type | Description | ------ | ------ | ------ | `value` | S | The value to use in the strict equal check | **Returns:** *[ISimpleType](interfaces/isimpletype.md)‹S›* ___ ### map ▸ **map**<**IT**>(`subtype`: IT): *IMapType‹IT›* *Defined in [src/types/complex-types/map.ts:511](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/map.ts#L511)* `types.map` - Creates a key based collection type who's children are all of a uniform declared type. If the type stored in a map has an identifier, it is mandatory to store the child under that identifier in the map. This type will always produce [observable maps](https://mobx.js.org/api.html#observablemap) Example: ```ts const Todo = types.model({ id: types.identifier, task: types.string }) const TodoStore = types.model({ todos: types.map(Todo) }) const s = TodoStore.create({ todos: {} }) unprotect(s) s.todos.set(17, { task: "Grab coffee", id: 17 }) s.todos.put({ task: "Grab cookie", id: 18 }) // put will infer key from the identifier console.log(s.todos.get(17).task) // prints: "Grab coffee" ``` **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** Name | Type | ------ | ------ | `subtype` | IT | **Returns:** *IMapType‹IT›* ___ ### maybe ▸ **maybe**<**IT**>(`type`: IT): *IMaybe‹IT›* *Defined in [src/types/utility-types/maybe.ts:31](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/maybe.ts#L31)* `types.maybe` - Maybe will make a type nullable, and also optional. The value `undefined` will be used to represent nullability. **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** Name | Type | ------ | ------ | `type` | IT | **Returns:** *IMaybe‹IT›* ___ ### maybeNull ▸ **maybeNull**<**IT**>(`type`: IT): *IMaybeNull‹IT›* *Defined in [src/types/utility-types/maybe.ts:44](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/maybe.ts#L44)* `types.maybeNull` - Maybe will make a type nullable, and also optional. The value `null` will be used to represent no value. **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** Name | Type | ------ | ------ | `type` | IT | **Returns:** *IMaybeNull‹IT›* ___ ### model ▸ **model**<**P**>(`name`: string, `properties?`: [P](undefined)): *[IModelType](interfaces/imodeltype.md)‹ModelPropertiesDeclarationToProperties‹P›, __type›* *Defined in [src/types/complex-types/model.ts:781](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L781)* `types.model` - Creates a new model type by providing a name, properties, volatile state and actions. See the [model type](/concepts/trees#creating-models) description or the [getting started](intro/getting-started.md#getting-started-1) tutorial. **Type parameters:** ▪ **P**: *ModelPropertiesDeclaration* **Parameters:** Name | Type | ------ | ------ | `name` | string | `properties?` | [P](undefined) | **Returns:** *[IModelType](interfaces/imodeltype.md)‹ModelPropertiesDeclarationToProperties‹P›, __type›* ▸ **model**<**P**>(`properties?`: [P](undefined)): *[IModelType](interfaces/imodeltype.md)‹ModelPropertiesDeclarationToProperties‹P›, __type›* *Defined in [src/types/complex-types/model.ts:785](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L785)* `types.model` - Creates a new model type by providing a name, properties, volatile state and actions. See the [model type](/concepts/trees#creating-models) description or the [getting started](intro/getting-started.md#getting-started-1) tutorial. **Type parameters:** ▪ **P**: *ModelPropertiesDeclaration* **Parameters:** Name | Type | ------ | ------ | `properties?` | [P](undefined) | **Returns:** *[IModelType](interfaces/imodeltype.md)‹ModelPropertiesDeclarationToProperties‹P›, __type›* ___ ### onAction ▸ **onAction**(`target`: IAnyStateTreeNode, `listener`: function, `attachAfter`: boolean): *[IDisposer](index.md#idisposer)* *Defined in [src/middlewares/on-action.ts:226](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L226)* Registers a function that will be invoked for each action that is called on the provided model instance, or to any of its children. See [actions](https://github.com/mobxjs/mobx-state-tree#actions) for more details. onAction events are emitted only for the outermost called action in the stack. Action can also be intercepted by middleware using addMiddleware to change the function call before it will be run. Not all action arguments might be serializable. For unserializable arguments, a struct like `{ $MST_UNSERIALIZABLE: true, type: "someType" }` will be generated. MST Nodes are considered non-serializable as well (they could be serialized as there snapshot, but it is uncertain whether an replaying party will be able to handle such a non-instantiated snapshot). Rather, when using `onAction` middleware, one should consider in passing arguments which are 1: an id, 2: a (relative) path, or 3: a snapshot. Instead of a real MST node. Example: ```ts const Todo = types.model({ task: types.string }) const TodoStore = types.model({ todos: types.array(Todo) }).actions(self => ({ add(todo) { self.todos.push(todo); } })) const s = TodoStore.create({ todos: [] }) let disposer = onAction(s, (call) => { console.log(call); }) s.add({ task: "Grab a coffee" }) // Logs: { name: "add", path: "", args: [{ task: "Grab a coffee" }] } ``` **Parameters:** ▪ **target**: *IAnyStateTreeNode* ▪ **listener**: *function* ▸ (`call`: [ISerializedActionCall](interfaces/iserializedactioncall.md)): *void* **Parameters:** Name | Type | ------ | ------ | `call` | [ISerializedActionCall](interfaces/iserializedactioncall.md) | ▪`Default value` **attachAfter**: *boolean*= false (default false) fires the listener *after* the action has executed instead of before. **Returns:** *[IDisposer](index.md#idisposer)* ___ ### onPatch ▸ **onPatch**(`target`: IAnyStateTreeNode, `callback`: function): *[IDisposer](index.md#idisposer)* *Defined in [src/core/mst-operations.ts:83](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L83)* Registers a function that will be invoked for each mutation that is applied to the provided model instance, or to any of its children. See [patches](https://github.com/mobxjs/mobx-state-tree#patches) for more details. onPatch events are emitted immediately and will not await the end of a transaction. Patches can be used to deeply observe a model tree. **Parameters:** ▪ **target**: *IAnyStateTreeNode* the model instance from which to receive patches ▪ **callback**: *function* the callback that is invoked for each patch. The reversePatch is a patch that would actually undo the emitted patch ▸ (`patch`: [IJsonPatch](interfaces/ijsonpatch.md), `reversePatch`: [IJsonPatch](interfaces/ijsonpatch.md)): *void* **Parameters:** Name | Type | ------ | ------ | `patch` | [IJsonPatch](interfaces/ijsonpatch.md) | `reversePatch` | [IJsonPatch](interfaces/ijsonpatch.md) | **Returns:** *[IDisposer](index.md#idisposer)* function to remove the listener ___ ### onSnapshot ▸ **onSnapshot**<**S**>(`target`: IStateTreeNode‹[IType](interfaces/itype.md)‹any, S, any››, `callback`: function): *[IDisposer](index.md#idisposer)* *Defined in [src/core/mst-operations.ts:103](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L103)* Registers a function that is invoked whenever a new snapshot for the given model instance is available. The listener will only be fire at the end of the current MobX (trans)action. See [snapshots](https://github.com/mobxjs/mobx-state-tree#snapshots) for more details. **Type parameters:** ▪ **S** **Parameters:** ▪ **target**: *IStateTreeNode‹[IType](interfaces/itype.md)‹any, S, any››* ▪ **callback**: *function* ▸ (`snapshot`: S): *void* **Parameters:** Name | Type | ------ | ------ | `snapshot` | S | **Returns:** *[IDisposer](index.md#idisposer)* ___ ### optional ▸ **optional**<**IT**>(`type`: IT, `defaultValueOrFunction`: OptionalDefaultValueOrFunction‹IT›): *IOptionalIType‹IT, []›* *Defined in [src/types/utility-types/optional.ts:160](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/optional.ts#L160)* `types.optional` - Can be used to create a property with a default value. Depending on the third argument (`optionalValues`) there are two ways of operation: - If the argument is not provided, then if a value is not provided in the snapshot (`undefined` or missing), it will default to the provided `defaultValue` - If the argument is provided, then if the value in the snapshot matches one of the optional values inside the array then it will default to the provided `defaultValue`. Additionally, if one of the optional values inside the array is `undefined` then a missing property is also valid. Note that it is also possible to include values of the same type as the intended subtype as optional values, in this case the optional value will be transformed into the `defaultValue` (e.g. `types.optional(types.string, "unnamed", [undefined, ""])` will transform the snapshot values `undefined` (and therefore missing) and empty strings into the string `"unnamed"` when it gets instantiated). If `defaultValue` is a function, the function will be invoked for every new instance. Applying a snapshot in which the optional value is one of the optional values (or `undefined`/_not_ present if none are provided) causes the value to be reset. Example: ```ts const Todo = types.model({ title: types.string, subtitle1: types.optional(types.string, "", [null]), subtitle2: types.optional(types.string, "", [null, undefined]), done: types.optional(types.boolean, false), created: types.optional(types.Date, () => new Date()), }) // if done is missing / undefined it will become false // if created is missing / undefined it will get a freshly generated timestamp // if subtitle1 is null it will default to "", but it cannot be missing or undefined // if subtitle2 is null or undefined it will default to ""; since it can be undefined it can also be missing const todo = Todo.create({ title: "Get coffee", subtitle1: null }) ``` **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** Name | Type | ------ | ------ | `type` | IT | `defaultValueOrFunction` | OptionalDefaultValueOrFunction‹IT› | **Returns:** *IOptionalIType‹IT, []›* ▸ **optional**<**IT**, **OptionalVals**>(`type`: IT, `defaultValueOrFunction`: OptionalDefaultValueOrFunction‹IT›, `optionalValues`: OptionalVals): *IOptionalIType‹IT, OptionalVals›* *Defined in [src/types/utility-types/optional.ts:164](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/optional.ts#L164)* `types.optional` - Can be used to create a property with a default value. Depending on the third argument (`optionalValues`) there are two ways of operation: - If the argument is not provided, then if a value is not provided in the snapshot (`undefined` or missing), it will default to the provided `defaultValue` - If the argument is provided, then if the value in the snapshot matches one of the optional values inside the array then it will default to the provided `defaultValue`. Additionally, if one of the optional values inside the array is `undefined` then a missing property is also valid. Note that it is also possible to include values of the same type as the intended subtype as optional values, in this case the optional value will be transformed into the `defaultValue` (e.g. `types.optional(types.string, "unnamed", [undefined, ""])` will transform the snapshot values `undefined` (and therefore missing) and empty strings into the string `"unnamed"` when it gets instantiated). If `defaultValue` is a function, the function will be invoked for every new instance. Applying a snapshot in which the optional value is one of the optional values (or `undefined`/_not_ present if none are provided) causes the value to be reset. Example: ```ts const Todo = types.model({ title: types.string, subtitle1: types.optional(types.string, "", [null]), subtitle2: types.optional(types.string, "", [null, undefined]), done: types.optional(types.boolean, false), created: types.optional(types.Date, () => new Date()), }) // if done is missing / undefined it will become false // if created is missing / undefined it will get a freshly generated timestamp // if subtitle1 is null it will default to "", but it cannot be missing or undefined // if subtitle2 is null or undefined it will default to ""; since it can be undefined it can also be missing const todo = Todo.create({ title: "Get coffee", subtitle1: null }) ``` **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* ▪ **OptionalVals**: *ValidOptionalValues* **Parameters:** Name | Type | Description | ------ | ------ | ------ | `type` | IT | - | `defaultValueOrFunction` | OptionalDefaultValueOrFunction‹IT› | - | `optionalValues` | OptionalVals | an optional array with zero or more primitive values (string, number, boolean, null or undefined) that will be converted into the default. `[ undefined ]` is assumed when none is provided | **Returns:** *IOptionalIType‹IT, OptionalVals›* ___ ### protect ▸ **protect**(`target`: IAnyStateTreeNode): *void* *Defined in [src/core/mst-operations.ts:265](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L265)* The inverse of `unprotect`. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `target` | IAnyStateTreeNode | | **Returns:** *void* ___ ### recordActions ▸ **recordActions**(`subject`: IAnyStateTreeNode, `filter?`: undefined | function): *[IActionRecorder](interfaces/iactionrecorder.md)* *Defined in [src/middlewares/on-action.ts:148](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L148)* Small abstraction around `onAction` and `applyAction`, attaches an action listener to a tree and records all the actions emitted. Returns an recorder object with the following signature: Example: ```ts export interface IActionRecorder { // the recorded actions actions: ISerializedActionCall[] // true if currently recording recording: boolean // stop recording actions stop(): void // resume recording actions resume(): void // apply all the recorded actions on the given object replay(target: IAnyStateTreeNode): void } ``` The optional filter function allows to skip recording certain actions. **Parameters:** Name | Type | ------ | ------ | `subject` | IAnyStateTreeNode | `filter?` | undefined | function | **Returns:** *[IActionRecorder](interfaces/iactionrecorder.md)* ___ ### recordPatches ▸ **recordPatches**(`subject`: IAnyStateTreeNode, `filter?`: undefined | function): *[IPatchRecorder](interfaces/ipatchrecorder.md)* *Defined in [src/core/mst-operations.ts:177](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L177)* Small abstraction around `onPatch` and `applyPatch`, attaches a patch listener to a tree and records all the patches. Returns a recorder object with the following signature: Example: ```ts export interface IPatchRecorder { // the recorded patches patches: IJsonPatch[] // the inverse of the recorded patches inversePatches: IJsonPatch[] // true if currently recording recording: boolean // stop recording patches stop(): void // resume recording patches resume(): void // apply all the recorded patches on the given target (the original subject if omitted) replay(target?: IAnyStateTreeNode): void // reverse apply the recorded patches on the given target (the original subject if omitted) // stops the recorder if not already stopped undo(): void } ``` The optional filter function allows to skip recording certain patches. **Parameters:** Name | Type | ------ | ------ | `subject` | IAnyStateTreeNode | `filter?` | undefined | function | **Returns:** *[IPatchRecorder](interfaces/ipatchrecorder.md)* ___ ### reference ▸ **reference**<**IT**>(`subType`: IT, `options?`: [ReferenceOptions](index.md#referenceoptions)‹IT›): *IReferenceType‹IT›* *Defined in [src/types/utility-types/reference.ts:494](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L494)* `types.reference` - Creates a reference to another type, which should have defined an identifier. See also the [reference and identifiers](https://github.com/mobxjs/mobx-state-tree#references-and-identifiers) section. **Type parameters:** ▪ **IT**: *[IAnyComplexType](interfaces/ianycomplextype.md)* **Parameters:** Name | Type | ------ | ------ | `subType` | IT | `options?` | [ReferenceOptions](index.md#referenceoptions)‹IT› | **Returns:** *IReferenceType‹IT›* ___ ### refinement ▸ **refinement**<**IT**>(`name`: string, `type`: IT, `predicate`: function, `message?`: string | function): *IT* *Defined in [src/types/utility-types/refinement.ts:83](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/refinement.ts#L83)* `types.refinement` - Creates a type that is more specific than the base type, e.g. `types.refinement(types.string, value => value.length > 5)` to create a type of strings that can only be longer then 5. **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** ▪ **name**: *string* ▪ **type**: *IT* ▪ **predicate**: *function* ▸ (`snapshot`: IT["CreationType"]): *boolean* **Parameters:** Name | Type | ------ | ------ | `snapshot` | IT["CreationType"] | ▪`Optional` **message**: *string | function* **Returns:** *IT* ▸ **refinement**<**IT**>(`type`: IT, `predicate`: function, `message?`: string | function): *IT* *Defined in [src/types/utility-types/refinement.ts:89](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/refinement.ts#L89)* `types.refinement` - Creates a type that is more specific than the base type, e.g. `types.refinement(types.string, value => value.length > 5)` to create a type of strings that can only be longer then 5. **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** ▪ **type**: *IT* ▪ **predicate**: *function* ▸ (`snapshot`: IT["CreationType"]): *boolean* **Parameters:** Name | Type | ------ | ------ | `snapshot` | IT["CreationType"] | ▪`Optional` **message**: *string | function* **Returns:** *IT* ___ ### resolveIdentifier ▸ **resolveIdentifier**<**IT**>(`type`: IT, `target`: IAnyStateTreeNode, `identifier`: [ReferenceIdentifier](index.md#referenceidentifier)): *IT["Type"] | undefined* *Defined in [src/core/mst-operations.ts:525](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L525)* Resolves a model instance given a root target, the type and the identifier you are searching for. Returns undefined if no value can be found. **Type parameters:** ▪ **IT**: *[IAnyModelType](interfaces/ianymodeltype.md)* **Parameters:** Name | Type | ------ | ------ | `type` | IT | `target` | IAnyStateTreeNode | `identifier` | [ReferenceIdentifier](index.md#referenceidentifier) | **Returns:** *IT["Type"] | undefined* ___ ### resolvePath ▸ **resolvePath**(`target`: IAnyStateTreeNode, `path`: string): *any* *Defined in [src/core/mst-operations.ts:507](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L507)* Resolves a path relatively to a given object. Returns undefined if no value can be found. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `target` | IAnyStateTreeNode | - | `path` | string | escaped json path | **Returns:** *any* ___ ### safeReference ▸ **safeReference**<**IT**>(`subType`: IT, `options`: __type | [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› & object): *IReferenceType‹IT›* *Defined in [src/types/utility-types/reference.ts:545](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L545)* `types.safeReference` - A safe reference is like a standard reference, except that it accepts the undefined value by default and automatically sets itself to undefined (when the parent is a model) / removes itself from arrays and maps when the reference it is pointing to gets detached/destroyed. The optional options parameter object accepts a parameter named `acceptsUndefined`, which is set to true by default, so it is suitable for model properties. When used inside collections (arrays/maps), it is recommended to set this option to false so it can't take undefined as value, which is usually the desired in those cases. Additionally, the optional options parameter object accepts a parameter named `onInvalidated`, which will be called when the reference target node that the reference is pointing to is about to be detached/destroyed Strictly speaking it is a `types.maybe(types.reference(X))` (when `acceptsUndefined` is set to true, the default) and `types.reference(X)` (when `acceptsUndefined` is set to false), both of them with a customized `onInvalidated` option. **Type parameters:** ▪ **IT**: *[IAnyComplexType](interfaces/ianycomplextype.md)* **Parameters:** Name | Type | ------ | ------ | `subType` | IT | `options` | __type | [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› & object | **Returns:** *IReferenceType‹IT›* ▸ **safeReference**<**IT**>(`subType`: IT, `options?`: __type | [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› & object): *IMaybe‹IReferenceType‹IT››* *Defined in [src/types/utility-types/reference.ts:552](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L552)* `types.safeReference` - A safe reference is like a standard reference, except that it accepts the undefined value by default and automatically sets itself to undefined (when the parent is a model) / removes itself from arrays and maps when the reference it is pointing to gets detached/destroyed. The optional options parameter object accepts a parameter named `acceptsUndefined`, which is set to true by default, so it is suitable for model properties. When used inside collections (arrays/maps), it is recommended to set this option to false so it can't take undefined as value, which is usually the desired in those cases. Additionally, the optional options parameter object accepts a parameter named `onInvalidated`, which will be called when the reference target node that the reference is pointing to is about to be detached/destroyed Strictly speaking it is a `types.maybe(types.reference(X))` (when `acceptsUndefined` is set to true, the default) and `types.reference(X)` (when `acceptsUndefined` is set to false), both of them with a customized `onInvalidated` option. **Type parameters:** ▪ **IT**: *[IAnyComplexType](interfaces/ianycomplextype.md)* **Parameters:** Name | Type | ------ | ------ | `subType` | IT | `options?` | __type | [ReferenceOptionsGetSet](interfaces/referenceoptionsgetset.md)‹IT› & object | **Returns:** *IMaybe‹IReferenceType‹IT››* ___ ### setLivelinessChecking ▸ **setLivelinessChecking**(`mode`: [LivelinessMode](index.md#livelinessmode)): *void* *Defined in [src/core/node/livelinessChecking.ts:18](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/livelinessChecking.ts#L18)* Defines what MST should do when running into reads / writes to objects that have died. By default it will print a warning. Use the `"error"` option to easy debugging to see where the error was thrown and when the offending read / write took place **Parameters:** Name | Type | Description | ------ | ------ | ------ | `mode` | [LivelinessMode](index.md#livelinessmode) | `"warn"`, `"error"` or `"ignore"` | **Returns:** *void* ___ ### snapshotProcessor ▸ **snapshotProcessor**<**IT**, **CustomC**, **CustomS**>(`type`: IT, `processors`: [ISnapshotProcessors](interfaces/isnapshotprocessors.md)‹IT, CustomC, CustomS›, `name?`: undefined | string): *[ISnapshotProcessor](interfaces/isnapshotprocessor.md)‹IT, CustomC, CustomS›* *Defined in [src/types/utility-types/snapshotProcessor.ts:271](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/snapshotProcessor.ts#L271)* `types.snapshotProcessor` - Runs a pre/post snapshot processor before/after serializing a given type. [See known issue with `applySnapshot` and `preProcessSnapshot`](https://github.com/mobxjs/mobx-state-tree/issues/1317) Example: ```ts const Todo1 = types.model({ text: types.string }) // in the backend the text type must be null when empty interface BackendTodo { text: string | null } const Todo2 = types.snapshotProcessor(Todo1, { // from snapshot to instance preProcessor(snapshot: BackendTodo) { return { text: sn.text || ""; } }, // from instance to snapshot postProcessor(snapshot, node): BackendTodo { return { text: !sn.text ? null : sn.text } } }) ``` **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* ▪ **CustomC** ▪ **CustomS** **Parameters:** Name | Type | Description | ------ | ------ | ------ | `type` | IT | Type to run the processors over. | `processors` | [ISnapshotProcessors](interfaces/isnapshotprocessors.md)‹IT, CustomC, CustomS› | Processors to run. | `name?` | undefined | string | Type name, or undefined to inherit the inner type one. | **Returns:** *[ISnapshotProcessor](interfaces/isnapshotprocessor.md)‹IT, CustomC, CustomS›* ___ ### splitJsonPath ▸ **splitJsonPath**(`path`: string): *string[]* *Defined in [src/core/json-patch.ts:119](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L119)* Splits and decodes a json path into several parts. **Parameters:** Name | Type | ------ | ------ | `path` | string | **Returns:** *string[]* ___ ### toGenerator ▸ **toGenerator**<**R**>(`p`: Promise‹R›): *Generator‹Promise‹R›, R, R›* *Defined in [src/core/flow.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/flow.ts#L87)* **`experimental`** experimental api - might change on minor/patch releases Convert a promise to a generator yielding that promise This is intended to allow for usage of `yield*` in async actions to retain the promise return type. Example: ```ts function getDataAsync(input: string): Promise { ... } const someModel.actions(self => ({ someAction: flow(function*() { // value is typed as number const value = yield* toGenerator(getDataAsync("input value")); ... }) })) ``` **Type parameters:** ▪ **R** **Parameters:** Name | Type | ------ | ------ | `p` | Promise‹R› | **Returns:** *Generator‹Promise‹R›, R, R›* ___ ### toGeneratorFunction ▸ **toGeneratorFunction**<**R**, **Args**>(`p`: function): *(Anonymous function)* *Defined in [src/core/flow.ts:60](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/flow.ts#L60)* **`experimental`** experimental api - might change on minor/patch releases Convert a promise-returning function to a generator-returning one. This is intended to allow for usage of `yield*` in async actions to retain the promise return type. Example: ```ts function getDataAsync(input: string): Promise { ... } const getDataGen = toGeneratorFunction(getDataAsync); const someModel.actions(self => ({ someAction: flow(function*() { // value is typed as number const value = yield* getDataGen("input value"); ... }) })) ``` **Type parameters:** ▪ **R** ▪ **Args**: *any[]* **Parameters:** ▪ **p**: *function* ▸ (...`args`: Args): *Promise‹R›* **Parameters:** Name | Type | ------ | ------ | `...args` | Args | **Returns:** *(Anonymous function)* ___ ### tryReference ▸ **tryReference**<**N**>(`getter`: function, `checkIfAlive`: boolean): *N | undefined* *Defined in [src/core/mst-operations.ts:564](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L564)* Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns such reference if the check passes, else it returns undefined. **Type parameters:** ▪ **N**: *IAnyStateTreeNode* **Parameters:** ▪ **getter**: *function* Function to access the reference. ▸ (): *N | null | undefined* ▪`Default value` **checkIfAlive**: *boolean*= true true to also make sure the referenced node is alive (default), false to skip this check. **Returns:** *N | undefined* ___ ### tryResolve ▸ **tryResolve**(`target`: IAnyStateTreeNode, `path`: string): *any* *Defined in [src/core/mst-operations.ts:624](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L624)* Try to resolve a given path relative to a given node. **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | `path` | string | **Returns:** *any* ___ ### typecheck ▸ **typecheck**<**IT**>(`type`: IT, `value`: ExtractCSTWithSTN‹IT›): *void* *Defined in [src/core/type/type-checker.ts:164](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L164)* Run's the typechecker for the given type on the given value, which can be a snapshot or an instance. Throws if the given value is not according the provided type specification. Use this if you need typechecks even in a production build (by default all automatic runtime type checks will be skipped in production builds) **Type parameters:** ▪ **IT**: *[IAnyType](interfaces/ianytype.md)* **Parameters:** Name | Type | Description | ------ | ------ | ------ | `type` | IT | Type to check against. | `value` | ExtractCSTWithSTN‹IT› | Value to be checked, either a snapshot or an instance. | **Returns:** *void* ___ ### unescapeJsonPath ▸ **unescapeJsonPath**(`path`: string): *string* *Defined in [src/core/json-patch.ts:89](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L89)* Unescape slashes and backslashes. **Parameters:** Name | Type | ------ | ------ | `path` | string | **Returns:** *string* ___ ### union ▸ **union**<**Types**>(...`types`: Types): *[IUnionType](index.md#iuniontype)‹Types›* *Defined in [src/types/utility-types/union.ts:175](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L175)* `types.union` - Create a union of multiple types. If the correct type cannot be inferred unambiguously from a snapshot, provide a dispatcher function of the form `(snapshot) => Type`. **Type parameters:** ▪ **Types**: *[IAnyType](interfaces/ianytype.md)[]* **Parameters:** Name | Type | ------ | ------ | `...types` | Types | **Returns:** *[IUnionType](index.md#iuniontype)‹Types›* ▸ **union**<**Types**>(`options`: [UnionOptions](interfaces/unionoptions.md)‹Types›, ...`types`: Types): *[IUnionType](index.md#iuniontype)‹Types›* *Defined in [src/types/utility-types/union.ts:176](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L176)* `types.union` - Create a union of multiple types. If the correct type cannot be inferred unambiguously from a snapshot, provide a dispatcher function of the form `(snapshot) => Type`. **Type parameters:** ▪ **Types**: *[IAnyType](interfaces/ianytype.md)[]* **Parameters:** Name | Type | ------ | ------ | `options` | [UnionOptions](interfaces/unionoptions.md)‹Types› | `...types` | Types | **Returns:** *[IUnionType](index.md#iuniontype)‹Types›* ___ ### unprotect ▸ **unprotect**(`target`: IAnyStateTreeNode): *void* *Defined in [src/core/mst-operations.ts:298](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L298)* By default it is not allowed to directly modify a model. Models can only be modified through actions. However, in some cases you don't care about the advantages (like replayability, traceability, etc) this yields. For example because you are building a PoC or don't have any middleware attached to your tree. In that case you can disable this protection by calling `unprotect` on the root of your tree. Example: ```ts const Todo = types.model({ done: false }).actions(self => ({ toggle() { self.done = !self.done } })) const todo = Todo.create() todo.done = true // throws! todo.toggle() // OK unprotect(todo) todo.done = false // OK ``` **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *void* ___ ### walk ▸ **walk**(`target`: IAnyStateTreeNode, `processor`: function): *void* *Defined in [src/core/mst-operations.ts:808](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L808)* Performs a depth first walk through a tree. **Parameters:** ▪ **target**: *IAnyStateTreeNode* ▪ **processor**: *function* ▸ (`item`: IAnyStateTreeNode): *void* **Parameters:** Name | Type | ------ | ------ | `item` | IAnyStateTreeNode | **Returns:** *void* ## Object literals ### `Const` types ### ▪ **types**: *object* *Defined in [src/types/index.ts:34](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L34)* ### Date • **Date**: *[IType](interfaces/itype.md)‹number | Date, number, Date›* = DatePrimitive *Defined in [src/types/index.ts:53](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L53)* ### array • **array**: *[array](index.md#array)* *Defined in [src/types/index.ts:55](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L55)* ### boolean • **boolean**: *[ISimpleType](interfaces/isimpletype.md)‹boolean›* *Defined in [src/types/index.ts:48](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L48)* ### compose • **compose**: *[compose](index.md#compose)* *Defined in [src/types/index.ts:37](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L37)* ### custom • **custom**: *[custom](index.md#custom)* *Defined in [src/types/index.ts:38](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L38)* ### enumeration • **enumeration**: *[enumeration](index.md#enumeration)* *Defined in [src/types/index.ts:35](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L35)* ### finite • **finite**: *[ISimpleType](interfaces/isimpletype.md)‹number›* *Defined in [src/types/index.ts:52](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L52)* ### float • **float**: *[ISimpleType](interfaces/isimpletype.md)‹number›* *Defined in [src/types/index.ts:51](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L51)* ### frozen • **frozen**: *[frozen](index.md#frozen)* *Defined in [src/types/index.ts:56](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L56)* ### identifier • **identifier**: *[ISimpleType](interfaces/isimpletype.md)‹string›* *Defined in [src/types/index.ts:57](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L57)* ### identifierNumber • **identifierNumber**: *[ISimpleType](interfaces/isimpletype.md)‹number›* *Defined in [src/types/index.ts:58](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L58)* ### integer • **integer**: *[ISimpleType](interfaces/isimpletype.md)‹number›* *Defined in [src/types/index.ts:50](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L50)* ### late • **late**: *[late](index.md#late)* *Defined in [src/types/index.ts:59](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L59)* ### lazy • **lazy**: *[lazy](index.md#lazy)* *Defined in [src/types/index.ts:60](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L60)* ### literal • **literal**: *[literal](index.md#literal)* *Defined in [src/types/index.ts:43](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L43)* ### map • **map**: *[map](index.md#map)* *Defined in [src/types/index.ts:54](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L54)* ### maybe • **maybe**: *[maybe](index.md#maybe)* *Defined in [src/types/index.ts:44](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L44)* ### maybeNull • **maybeNull**: *[maybeNull](index.md#maybenull)* *Defined in [src/types/index.ts:45](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L45)* ### model • **model**: *[model](index.md#model)* *Defined in [src/types/index.ts:36](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L36)* ### null • **null**: *[ISimpleType](interfaces/isimpletype.md)‹null›* = nullType *Defined in [src/types/index.ts:62](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L62)* ### number • **number**: *[ISimpleType](interfaces/isimpletype.md)‹number›* *Defined in [src/types/index.ts:49](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L49)* ### optional • **optional**: *[optional](index.md#optional)* *Defined in [src/types/index.ts:42](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L42)* ### reference • **reference**: *[reference](index.md#reference)* *Defined in [src/types/index.ts:39](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L39)* ### refinement • **refinement**: *[refinement](index.md#refinement)* *Defined in [src/types/index.ts:46](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L46)* ### safeReference • **safeReference**: *[safeReference](index.md#safereference)* *Defined in [src/types/index.ts:40](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L40)* ### snapshotProcessor • **snapshotProcessor**: *[snapshotProcessor](index.md#snapshotprocessor)* *Defined in [src/types/index.ts:63](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L63)* ### string • **string**: *[ISimpleType](interfaces/isimpletype.md)‹string›* *Defined in [src/types/index.ts:47](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L47)* ### undefined • **undefined**: *[ISimpleType](interfaces/isimpletype.md)‹undefined›* = undefinedType *Defined in [src/types/index.ts:61](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L61)* ### union • **union**: *[union](index.md#union)* *Defined in [src/types/index.ts:41](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/index.ts#L41)* ================================================ FILE: docs/API/interfaces/customtypeoptions.md ================================================ --- id: "customtypeoptions" title: "CustomTypeOptions" sidebar_label: "CustomTypeOptions" --- [mobx-state-tree - v7.0.2](../index.md) › [CustomTypeOptions](customtypeoptions.md) ## Type parameters ▪ **S** ▪ **T** ## Hierarchy * **CustomTypeOptions** ## Index ### Properties * [name](customtypeoptions.md#name) ### Methods * [fromSnapshot](customtypeoptions.md#fromsnapshot) * [getValidationMessage](customtypeoptions.md#getvalidationmessage) * [isTargetType](customtypeoptions.md#istargettype) * [toSnapshot](customtypeoptions.md#tosnapshot) ## Properties ### name • **name**: *string* *Defined in [src/types/utility-types/custom.ts:15](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/custom.ts#L15)* Friendly name ## Methods ### fromSnapshot ▸ **fromSnapshot**(`snapshot`: S, `env?`: any): *T* *Defined in [src/types/utility-types/custom.ts:17](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/custom.ts#L17)* given a serialized value and environment, how to turn it into the target type **Parameters:** Name | Type | ------ | ------ | `snapshot` | S | `env?` | any | **Returns:** *T* ___ ### getValidationMessage ▸ **getValidationMessage**(`snapshot`: S): *string* *Defined in [src/types/utility-types/custom.ts:23](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/custom.ts#L23)* a non empty string is assumed to be a validation error **Parameters:** Name | Type | ------ | ------ | `snapshot` | S | **Returns:** *string* ___ ### isTargetType ▸ **isTargetType**(`value`: T | S): *boolean* *Defined in [src/types/utility-types/custom.ts:21](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/custom.ts#L21)* if true, this is a converted value, if false, it's a snapshot **Parameters:** Name | Type | ------ | ------ | `value` | T | S | **Returns:** *boolean* ___ ### toSnapshot ▸ **toSnapshot**(`value`: T): *S* *Defined in [src/types/utility-types/custom.ts:19](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/custom.ts#L19)* return the serialization of the current value **Parameters:** Name | Type | ------ | ------ | `value` | T | **Returns:** *S* ================================================ FILE: docs/API/interfaces/functionwithflag.md ================================================ --- id: "functionwithflag" title: "FunctionWithFlag" sidebar_label: "FunctionWithFlag" --- [mobx-state-tree - v7.0.2](../index.md) › [FunctionWithFlag](functionwithflag.md) ## Hierarchy * Function ↳ **FunctionWithFlag** ## Index ### Properties * [Function](functionwithflag.md#function) * [[Symbol.metadata]](functionwithflag.md#[symbol.metadata]) * [_isFlowAction](functionwithflag.md#optional-_isflowaction) * [_isMSTAction](functionwithflag.md#optional-_ismstaction) * [arguments](functionwithflag.md#arguments) * [caller](functionwithflag.md#caller) * [length](functionwithflag.md#length) * [name](functionwithflag.md#name) * [prototype](functionwithflag.md#prototype) ### Methods * [[Symbol.hasInstance]](functionwithflag.md#[symbol.hasinstance]) * [apply](functionwithflag.md#apply) * [bind](functionwithflag.md#bind) * [call](functionwithflag.md#call) * [toString](functionwithflag.md#tostring) ## Properties ### Function • **Function**: *FunctionConstructor* Defined in node_modules/typescript/lib/lib.es5.d.ts:319 ___ ### [Symbol.metadata] • **[Symbol.metadata]**: *DecoratorMetadata | null* *Inherited from void* Defined in node_modules/typescript/lib/lib.esnext.decorators.d.ts:27 ___ ### `Optional` _isFlowAction • **_isFlowAction**? : *undefined | false | true* *Defined in [src/core/action.ts:42](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L42)* ___ ### `Optional` _isMSTAction • **_isMSTAction**? : *undefined | false | true* *Defined in [src/core/action.ts:41](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L41)* ___ ### arguments • **arguments**: *any* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:305 ___ ### caller • **caller**: *Function* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:306 ___ ### length • **length**: *number* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:302 ___ ### name • **name**: *string* *Inherited from void* Defined in node_modules/typescript/lib/lib.es2015.core.d.ts:97 Returns the name of the function. Function names are read-only and can not be changed. ___ ### prototype • **prototype**: *any* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:301 ## Methods ### [Symbol.hasInstance] ▸ **[Symbol.hasInstance]**(`value`: any): *boolean* *Inherited from void* Defined in node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts:164 Determines whether the given value inherits from this function if this function was used as a constructor function. A constructor function can control which objects are recognized as its instances by 'instanceof' by overriding this method. **Parameters:** Name | Type | ------ | ------ | `value` | any | **Returns:** *boolean* ___ ### apply ▸ **apply**(`this`: Function, `thisArg`: any, `argArray?`: any): *any* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:281 Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `this` | Function | - | `thisArg` | any | The object to be used as the this object. | `argArray?` | any | A set of arguments to be passed to the function. | **Returns:** *any* ___ ### bind ▸ **bind**(`this`: Function, `thisArg`: any, ...`argArray`: any[]): *any* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:296 For a given function, creates a bound function that has the same body as the original function. The this object of the bound function is associated with the specified object, and has the specified initial parameters. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `this` | Function | - | `thisArg` | any | An object to which the this keyword can refer inside the new function. | `...argArray` | any[] | A list of arguments to be passed to the new function. | **Returns:** *any* ___ ### call ▸ **call**(`this`: Function, `thisArg`: any, ...`argArray`: any[]): *any* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:288 Calls a method of an object, substituting another object for the current object. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `this` | Function | - | `thisArg` | any | The object to be used as the current object. | `...argArray` | any[] | A list of arguments to be passed to the method. | **Returns:** *any* ___ ### toString ▸ **toString**(): *string* *Inherited from void* Defined in node_modules/typescript/lib/lib.es5.d.ts:299 Returns a string representation of a function. **Returns:** *string* ================================================ FILE: docs/API/interfaces/iactioncontext.md ================================================ --- id: "iactioncontext" title: "IActionContext" sidebar_label: "IActionContext" --- [mobx-state-tree - v7.0.2](../index.md) › [IActionContext](iactioncontext.md) ## Hierarchy * **IActionContext** ↳ [IMiddlewareEvent](imiddlewareevent.md) ## Index ### Properties * [args](iactioncontext.md#args) * [context](iactioncontext.md#context) * [id](iactioncontext.md#id) * [name](iactioncontext.md#name) * [parentActionEvent](iactioncontext.md#parentactionevent) * [tree](iactioncontext.md#tree) ## Properties ### args • **args**: *any[]* *Defined in [src/core/actionContext.ts:20](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L20)* Event arguments in an array (action arguments for actions) ___ ### context • **context**: *IAnyStateTreeNode* *Defined in [src/core/actionContext.ts:15](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L15)* Event context (node where the action was invoked) ___ ### id • **id**: *number* *Defined in [src/core/actionContext.ts:9](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L9)* Event unique id ___ ### name • **name**: *string* *Defined in [src/core/actionContext.ts:6](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L6)* Event name (action name for actions) ___ ### parentActionEvent • **parentActionEvent**: *[IMiddlewareEvent](imiddlewareevent.md) | undefined* *Defined in [src/core/actionContext.ts:12](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L12)* Parent action event object ___ ### tree • **tree**: *IAnyStateTreeNode* *Defined in [src/core/actionContext.ts:17](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L17)* Event tree (root node of the node where the action was invoked) ================================================ FILE: docs/API/interfaces/iactionrecorder.md ================================================ --- id: "iactionrecorder" title: "IActionRecorder" sidebar_label: "IActionRecorder" --- [mobx-state-tree - v7.0.2](../index.md) › [IActionRecorder](iactionrecorder.md) ## Hierarchy * **IActionRecorder** ## Index ### Properties * [actions](iactionrecorder.md#actions) * [recording](iactionrecorder.md#recording) ### Methods * [replay](iactionrecorder.md#replay) * [resume](iactionrecorder.md#resume) * [stop](iactionrecorder.md#stop) ## Properties ### actions • **actions**: *ReadonlyArray‹[ISerializedActionCall](iserializedactioncall.md)›* *Defined in [src/middlewares/on-action.ts:37](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L37)* ___ ### recording • **recording**: *boolean* *Defined in [src/middlewares/on-action.ts:38](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L38)* ## Methods ### replay ▸ **replay**(`target`: IAnyStateTreeNode): *void* *Defined in [src/middlewares/on-action.ts:41](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L41)* **Parameters:** Name | Type | ------ | ------ | `target` | IAnyStateTreeNode | **Returns:** *void* ___ ### resume ▸ **resume**(): *void* *Defined in [src/middlewares/on-action.ts:40](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L40)* **Returns:** *void* ___ ### stop ▸ **stop**(): *void* *Defined in [src/middlewares/on-action.ts:39](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L39)* **Returns:** *void* ================================================ FILE: docs/API/interfaces/iactiontrackingmiddleware2call.md ================================================ --- id: "iactiontrackingmiddleware2call" title: "IActionTrackingMiddleware2Call" sidebar_label: "IActionTrackingMiddleware2Call" --- [mobx-state-tree - v7.0.2](../index.md) › [IActionTrackingMiddleware2Call](iactiontrackingmiddleware2call.md) ## Type parameters ▪ **TEnv** ## Hierarchy * object ↳ **IActionTrackingMiddleware2Call** ## Index ### Properties * [env](iactiontrackingmiddleware2call.md#env) * [parentCall](iactiontrackingmiddleware2call.md#optional-parentcall) ## Properties ### env • **env**: *TEnv | undefined* *Defined in [src/middlewares/createActionTrackingMiddleware2.ts:4](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/createActionTrackingMiddleware2.ts#L4)* ___ ### `Optional` parentCall • **parentCall**? : *[IActionTrackingMiddleware2Call](iactiontrackingmiddleware2call.md)‹TEnv›* *Defined in [src/middlewares/createActionTrackingMiddleware2.ts:5](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/createActionTrackingMiddleware2.ts#L5)* ================================================ FILE: docs/API/interfaces/iactiontrackingmiddleware2hooks.md ================================================ --- id: "iactiontrackingmiddleware2hooks" title: "IActionTrackingMiddleware2Hooks" sidebar_label: "IActionTrackingMiddleware2Hooks" --- [mobx-state-tree - v7.0.2](../index.md) › [IActionTrackingMiddleware2Hooks](iactiontrackingmiddleware2hooks.md) ## Type parameters ▪ **TEnv** ## Hierarchy * **IActionTrackingMiddleware2Hooks** ## Index ### Properties * [filter](iactiontrackingmiddleware2hooks.md#optional-filter) * [onFinish](iactiontrackingmiddleware2hooks.md#onfinish) * [onStart](iactiontrackingmiddleware2hooks.md#onstart) ## Properties ### `Optional` filter • **filter**? : *undefined | function* *Defined in [src/middlewares/createActionTrackingMiddleware2.ts:9](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/createActionTrackingMiddleware2.ts#L9)* ___ ### onFinish • **onFinish**: *function* *Defined in [src/middlewares/createActionTrackingMiddleware2.ts:11](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/createActionTrackingMiddleware2.ts#L11)* #### Type declaration: ▸ (`call`: [IActionTrackingMiddleware2Call](iactiontrackingmiddleware2call.md)‹TEnv›, `error?`: any): *void* **Parameters:** Name | Type | ------ | ------ | `call` | [IActionTrackingMiddleware2Call](iactiontrackingmiddleware2call.md)‹TEnv› | `error?` | any | ___ ### onStart • **onStart**: *function* *Defined in [src/middlewares/createActionTrackingMiddleware2.ts:10](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/createActionTrackingMiddleware2.ts#L10)* #### Type declaration: ▸ (`call`: [IActionTrackingMiddleware2Call](iactiontrackingmiddleware2call.md)‹TEnv›): *void* **Parameters:** Name | Type | ------ | ------ | `call` | [IActionTrackingMiddleware2Call](iactiontrackingmiddleware2call.md)‹TEnv› | ================================================ FILE: docs/API/interfaces/iactiontrackingmiddlewarehooks.md ================================================ --- id: "iactiontrackingmiddlewarehooks" title: "IActionTrackingMiddlewareHooks" sidebar_label: "IActionTrackingMiddlewareHooks" --- [mobx-state-tree - v7.0.2](../index.md) › [IActionTrackingMiddlewareHooks](iactiontrackingmiddlewarehooks.md) ## Type parameters ▪ **T** ## Hierarchy * **IActionTrackingMiddlewareHooks** ## Index ### Properties * [filter](iactiontrackingmiddlewarehooks.md#optional-filter) * [onFail](iactiontrackingmiddlewarehooks.md#onfail) * [onResume](iactiontrackingmiddlewarehooks.md#onresume) * [onStart](iactiontrackingmiddlewarehooks.md#onstart) * [onSuccess](iactiontrackingmiddlewarehooks.md#onsuccess) * [onSuspend](iactiontrackingmiddlewarehooks.md#onsuspend) ## Properties ### `Optional` filter • **filter**? : *undefined | function* *Defined in [src/middlewares/create-action-tracking-middleware.ts:6](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/create-action-tracking-middleware.ts#L6)* ___ ### onFail • **onFail**: *function* *Defined in [src/middlewares/create-action-tracking-middleware.ts:11](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/create-action-tracking-middleware.ts#L11)* #### Type declaration: ▸ (`call`: [IMiddlewareEvent](imiddlewareevent.md), `context`: T, `error`: any): *void* **Parameters:** Name | Type | ------ | ------ | `call` | [IMiddlewareEvent](imiddlewareevent.md) | `context` | T | `error` | any | ___ ### onResume • **onResume**: *function* *Defined in [src/middlewares/create-action-tracking-middleware.ts:8](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/create-action-tracking-middleware.ts#L8)* #### Type declaration: ▸ (`call`: [IMiddlewareEvent](imiddlewareevent.md), `context`: T): *void* **Parameters:** Name | Type | ------ | ------ | `call` | [IMiddlewareEvent](imiddlewareevent.md) | `context` | T | ___ ### onStart • **onStart**: *function* *Defined in [src/middlewares/create-action-tracking-middleware.ts:7](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/create-action-tracking-middleware.ts#L7)* #### Type declaration: ▸ (`call`: [IMiddlewareEvent](imiddlewareevent.md)): *T* **Parameters:** Name | Type | ------ | ------ | `call` | [IMiddlewareEvent](imiddlewareevent.md) | ___ ### onSuccess • **onSuccess**: *function* *Defined in [src/middlewares/create-action-tracking-middleware.ts:10](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/create-action-tracking-middleware.ts#L10)* #### Type declaration: ▸ (`call`: [IMiddlewareEvent](imiddlewareevent.md), `context`: T, `result`: any): *void* **Parameters:** Name | Type | ------ | ------ | `call` | [IMiddlewareEvent](imiddlewareevent.md) | `context` | T | `result` | any | ___ ### onSuspend • **onSuspend**: *function* *Defined in [src/middlewares/create-action-tracking-middleware.ts:9](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/create-action-tracking-middleware.ts#L9)* #### Type declaration: ▸ (`call`: [IMiddlewareEvent](imiddlewareevent.md), `context`: T): *void* **Parameters:** Name | Type | ------ | ------ | `call` | [IMiddlewareEvent](imiddlewareevent.md) | `context` | T | ================================================ FILE: docs/API/interfaces/ianycomplextype.md ================================================ --- id: "ianycomplextype" title: "IAnyComplexType" sidebar_label: "IAnyComplexType" --- [mobx-state-tree - v7.0.2](../index.md) › [IAnyComplexType](ianycomplextype.md) Any kind of complex type. ## Hierarchy * [IType](itype.md)‹any, any, object› ↳ **IAnyComplexType** ## Index ### Properties * [identifierAttribute](ianycomplextype.md#optional-identifierattribute) * [name](ianycomplextype.md#name) ### Methods * [create](ianycomplextype.md#create) * [describe](ianycomplextype.md#describe) * [is](ianycomplextype.md#is) * [validate](ianycomplextype.md#validate) ## Properties ### `Optional` identifierAttribute • **identifierAttribute**? : *undefined | string* *Inherited from [IType](itype.md).[identifierAttribute](itype.md#optional-identifierattribute)* *Defined in [src/core/type/type.ts:92](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L92)* Name of the identifier attribute or null if none. ___ ### name • **name**: *string* *Inherited from [IType](itype.md).[name](itype.md#name)* *Defined in [src/core/type/type.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L87)* Friendly type name. ## Methods ### create ▸ **create**(`snapshot?`: any | ExcludeReadonly‹object›, `env?`: any): *this["Type"]* *Inherited from [IType](itype.md).[create](itype.md#create)* *Defined in [src/core/type/type.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L99)* Creates an instance for the type given an snapshot input. **Parameters:** Name | Type | ------ | ------ | `snapshot?` | any | ExcludeReadonly‹object› | `env?` | any | **Returns:** *this["Type"]* An instance of that type. ___ ### describe ▸ **describe**(): *string* *Inherited from [IType](itype.md).[describe](itype.md#describe)* *Defined in [src/core/type/type.ts:121](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L121)* Gets the textual representation of the type as a string. **Returns:** *string* ___ ### is ▸ **is**(`thing`: any): *thing is any | this["Type"]* *Inherited from [IType](itype.md).[is](itype.md#is)* *Defined in [src/core/type/type.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L107)* Checks if a given snapshot / instance is of the given type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | Snapshot or instance to be checked. | **Returns:** *thing is any | this["Type"]* true if the value is of the current type, false otherwise. ___ ### validate ▸ **validate**(`thing`: any | object, `context`: [IValidationContext](../index.md#ivalidationcontext)): *[IValidationResult](../index.md#ivalidationresult)* *Inherited from [IType](itype.md).[validate](itype.md#validate)* *Defined in [src/core/type/type.ts:116](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L116)* Run's the type's typechecker on the given value with the given validation context. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | object | Value to be checked, either a snapshot or an instance. | `context` | [IValidationContext](../index.md#ivalidationcontext) | Validation context, an array of { subpaths, subtypes } that should be validated | **Returns:** *[IValidationResult](../index.md#ivalidationresult)* The validation result, an array with the list of validation errors. ================================================ FILE: docs/API/interfaces/ianymodeltype.md ================================================ --- id: "ianymodeltype" title: "IAnyModelType" sidebar_label: "IAnyModelType" --- [mobx-state-tree - v7.0.2](../index.md) › [IAnyModelType](ianymodeltype.md) Any model type. ## Hierarchy ↳ [IModelType](imodeltype.md)‹any, any, any, any› ↳ **IAnyModelType** ## Index ### Properties * [identifierAttribute](ianymodeltype.md#optional-identifierattribute) * [name](ianymodeltype.md#name) * [properties](ianymodeltype.md#properties) ### Methods * [actions](ianymodeltype.md#actions) * [create](ianymodeltype.md#create) * [describe](ianymodeltype.md#describe) * [extend](ianymodeltype.md#extend) * [is](ianymodeltype.md#is) * [named](ianymodeltype.md#named) * [postProcessSnapshot](ianymodeltype.md#postprocesssnapshot) * [preProcessSnapshot](ianymodeltype.md#preprocesssnapshot) * [props](ianymodeltype.md#props) * [validate](ianymodeltype.md#validate) * [views](ianymodeltype.md#views) * [volatile](ianymodeltype.md#volatile) ## Properties ### `Optional` identifierAttribute • **identifierAttribute**? : *undefined | string* *Inherited from [IType](itype.md).[identifierAttribute](itype.md#optional-identifierattribute)* *Defined in [src/core/type/type.ts:92](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L92)* Name of the identifier attribute or null if none. ___ ### name • **name**: *string* *Inherited from [IType](itype.md).[name](itype.md#name)* *Defined in [src/core/type/type.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L87)* Friendly type name. ___ ### properties • **properties**: *any* *Inherited from [IModelType](imodeltype.md).[properties](imodeltype.md#properties)* *Defined in [src/types/complex-types/model.ts:195](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L195)* ## Methods ### actions ▸ **actions**<**A**>(`fn`: function): *[IModelType](imodeltype.md)‹any, any & A, any, any›* *Inherited from [IModelType](imodeltype.md).[actions](imodeltype.md#actions)* *Defined in [src/types/complex-types/model.ts:209](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L209)* **Type parameters:** ▪ **A**: *ModelActions* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *A* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹any, any & A, any, any›* ___ ### create ▸ **create**(`snapshot?`: ModelCreationType2‹any, any› | ExcludeReadonly‹ModelInstanceType‹any, any››, `env?`: any): *this["Type"]* *Inherited from [IType](itype.md).[create](itype.md#create)* *Defined in [src/core/type/type.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L99)* Creates an instance for the type given an snapshot input. **Parameters:** Name | Type | ------ | ------ | `snapshot?` | ModelCreationType2‹any, any› | ExcludeReadonly‹ModelInstanceType‹any, any›› | `env?` | any | **Returns:** *this["Type"]* An instance of that type. ___ ### describe ▸ **describe**(): *string* *Inherited from [IType](itype.md).[describe](itype.md#describe)* *Defined in [src/core/type/type.ts:121](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L121)* Gets the textual representation of the type as a string. **Returns:** *string* ___ ### extend ▸ **extend**<**A**, **V**, **VS**>(`fn`: function): *[IModelType](imodeltype.md)‹any, any & A & V & VS, any, any›* *Inherited from [IModelType](imodeltype.md).[extend](imodeltype.md#extend)* *Defined in [src/types/complex-types/model.ts:217](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L217)* **Type parameters:** ▪ **A**: *ModelActions* ▪ **V**: *Object* ▪ **VS**: *Object* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *object* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹any, any & A & V & VS, any, any›* ___ ### is ▸ **is**(`thing`: any): *thing is ModelCreationType2 | this["Type"]* *Inherited from [IType](itype.md).[is](itype.md#is)* *Defined in [src/core/type/type.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L107)* Checks if a given snapshot / instance is of the given type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | Snapshot or instance to be checked. | **Returns:** *thing is ModelCreationType2 | this["Type"]* true if the value is of the current type, false otherwise. ___ ### named ▸ **named**(`newName`: string): *[IModelType](imodeltype.md)‹any, any, any, any›* *Inherited from [IModelType](imodeltype.md).[named](imodeltype.md#named)* *Defined in [src/types/complex-types/model.ts:197](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L197)* **Parameters:** Name | Type | ------ | ------ | `newName` | string | **Returns:** *[IModelType](imodeltype.md)‹any, any, any, any›* ___ ### postProcessSnapshot ▸ **postProcessSnapshot**<**NewS**>(`fn`: function): *[IModelType](imodeltype.md)‹any, any, any, NewS›* *Inherited from [IModelType](imodeltype.md).[postProcessSnapshot](imodeltype.md#postprocesssnapshot)* *Defined in [src/types/complex-types/model.ts:225](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L225)* **Type parameters:** ▪ **NewS** **Parameters:** ▪ **fn**: *function* ▸ (`snapshot`: ModelSnapshotType2‹any, any›): *NewS* **Parameters:** Name | Type | ------ | ------ | `snapshot` | ModelSnapshotType2‹any, any› | **Returns:** *[IModelType](imodeltype.md)‹any, any, any, NewS›* ___ ### preProcessSnapshot ▸ **preProcessSnapshot**<**NewC**>(`fn`: function): *[IModelType](imodeltype.md)‹any, any, NewC, any›* *Inherited from [IModelType](imodeltype.md).[preProcessSnapshot](imodeltype.md#preprocesssnapshot)* *Defined in [src/types/complex-types/model.ts:221](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L221)* **Type parameters:** ▪ **NewC** **Parameters:** ▪ **fn**: *function* ▸ (`snapshot`: NewC): *WithAdditionalProperties‹ModelCreationType2‹any, any››* **Parameters:** Name | Type | ------ | ------ | `snapshot` | NewC | **Returns:** *[IModelType](imodeltype.md)‹any, any, NewC, any›* ___ ### props ▸ **props**<**PROPS2**>(`props`: PROPS2): *[IModelType](imodeltype.md)‹any & ModelPropertiesDeclarationToProperties‹PROPS2›, any, any, any›* *Inherited from [IModelType](imodeltype.md).[props](imodeltype.md#props)* *Defined in [src/types/complex-types/model.ts:201](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L201)* **Type parameters:** ▪ **PROPS2**: *ModelPropertiesDeclaration* **Parameters:** Name | Type | ------ | ------ | `props` | PROPS2 | **Returns:** *[IModelType](imodeltype.md)‹any & ModelPropertiesDeclarationToProperties‹PROPS2›, any, any, any›* ___ ### validate ▸ **validate**(`thing`: ModelCreationType2‹any, any› | ModelInstanceType‹any, any›, `context`: [IValidationContext](../index.md#ivalidationcontext)): *[IValidationResult](../index.md#ivalidationresult)* *Inherited from [IType](itype.md).[validate](itype.md#validate)* *Defined in [src/core/type/type.ts:116](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L116)* Run's the type's typechecker on the given value with the given validation context. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | ModelCreationType2‹any, any› | ModelInstanceType‹any, any› | Value to be checked, either a snapshot or an instance. | `context` | [IValidationContext](../index.md#ivalidationcontext) | Validation context, an array of { subpaths, subtypes } that should be validated | **Returns:** *[IValidationResult](../index.md#ivalidationresult)* The validation result, an array with the list of validation errors. ___ ### views ▸ **views**<**V**>(`fn`: function): *[IModelType](imodeltype.md)‹any, any & V, any, any›* *Inherited from [IModelType](imodeltype.md).[views](imodeltype.md#views)* *Defined in [src/types/complex-types/model.ts:205](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L205)* **Type parameters:** ▪ **V**: *Object* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *V* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹any, any & V, any, any›* ___ ### volatile ▸ **volatile**<**TP**>(`fn`: function): *[IModelType](imodeltype.md)‹any, any & TP, any, any›* *Inherited from [IModelType](imodeltype.md).[volatile](imodeltype.md#volatile)* *Defined in [src/types/complex-types/model.ts:213](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L213)* **Type parameters:** ▪ **TP**: *object* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *TP* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹any, any & TP, any, any›* ================================================ FILE: docs/API/interfaces/ianytype.md ================================================ --- id: "ianytype" title: "IAnyType" sidebar_label: "IAnyType" --- [mobx-state-tree - v7.0.2](../index.md) › [IAnyType](ianytype.md) Any kind of type. ## Hierarchy * [IType](itype.md)‹any, any, any› ↳ **IAnyType** ## Index ### Properties * [identifierAttribute](ianytype.md#optional-identifierattribute) * [name](ianytype.md#name) ### Methods * [create](ianytype.md#create) * [describe](ianytype.md#describe) * [is](ianytype.md#is) * [validate](ianytype.md#validate) ## Properties ### `Optional` identifierAttribute • **identifierAttribute**? : *undefined | string* *Inherited from [IType](itype.md).[identifierAttribute](itype.md#optional-identifierattribute)* *Defined in [src/core/type/type.ts:92](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L92)* Name of the identifier attribute or null if none. ___ ### name • **name**: *string* *Inherited from [IType](itype.md).[name](itype.md#name)* *Defined in [src/core/type/type.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L87)* Friendly type name. ## Methods ### create ▸ **create**(`snapshot?`: any | ExcludeReadonly‹any›, `env?`: any): *this["Type"]* *Inherited from [IType](itype.md).[create](itype.md#create)* *Defined in [src/core/type/type.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L99)* Creates an instance for the type given an snapshot input. **Parameters:** Name | Type | ------ | ------ | `snapshot?` | any | ExcludeReadonly‹any› | `env?` | any | **Returns:** *this["Type"]* An instance of that type. ___ ### describe ▸ **describe**(): *string* *Inherited from [IType](itype.md).[describe](itype.md#describe)* *Defined in [src/core/type/type.ts:121](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L121)* Gets the textual representation of the type as a string. **Returns:** *string* ___ ### is ▸ **is**(`thing`: any): *thing is any | this["Type"]* *Inherited from [IType](itype.md).[is](itype.md#is)* *Defined in [src/core/type/type.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L107)* Checks if a given snapshot / instance is of the given type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | Snapshot or instance to be checked. | **Returns:** *thing is any | this["Type"]* true if the value is of the current type, false otherwise. ___ ### validate ▸ **validate**(`thing`: any | any, `context`: [IValidationContext](../index.md#ivalidationcontext)): *[IValidationResult](../index.md#ivalidationresult)* *Inherited from [IType](itype.md).[validate](itype.md#validate)* *Defined in [src/core/type/type.ts:116](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L116)* Run's the type's typechecker on the given value with the given validation context. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | any | Value to be checked, either a snapshot or an instance. | `context` | [IValidationContext](../index.md#ivalidationcontext) | Validation context, an array of { subpaths, subtypes } that should be validated | **Returns:** *[IValidationResult](../index.md#ivalidationresult)* The validation result, an array with the list of validation errors. ================================================ FILE: docs/API/interfaces/ihooks.md ================================================ --- id: "ihooks" title: "IHooks" sidebar_label: "IHooks" --- [mobx-state-tree - v7.0.2](../index.md) › [IHooks](ihooks.md) ## Hierarchy * **IHooks** ## Index ### Properties * [[Hook.afterAttach]](ihooks.md#optional-[hook.afterattach]) * [[Hook.afterCreate]](ihooks.md#optional-[hook.aftercreate]) * [[Hook.beforeDestroy]](ihooks.md#optional-[hook.beforedestroy]) * [[Hook.beforeDetach]](ihooks.md#optional-[hook.beforedetach]) ## Properties ### `Optional` [Hook.afterAttach] • **[Hook.afterAttach]**? : *undefined | function* *Defined in [src/core/node/Hook.ts:14](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/Hook.ts#L14)* ___ ### `Optional` [Hook.afterCreate] • **[Hook.afterCreate]**? : *undefined | function* *Defined in [src/core/node/Hook.ts:13](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/Hook.ts#L13)* ___ ### `Optional` [Hook.beforeDestroy] • **[Hook.beforeDestroy]**? : *undefined | function* *Defined in [src/core/node/Hook.ts:16](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/Hook.ts#L16)* ___ ### `Optional` [Hook.beforeDetach] • **[Hook.beforeDetach]**? : *undefined | function* *Defined in [src/core/node/Hook.ts:15](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/node/Hook.ts#L15)* ================================================ FILE: docs/API/interfaces/ijsonpatch.md ================================================ --- id: "ijsonpatch" title: "IJsonPatch" sidebar_label: "IJsonPatch" --- [mobx-state-tree - v7.0.2](../index.md) › [IJsonPatch](ijsonpatch.md) https://tools.ietf.org/html/rfc6902 http://jsonpatch.com/ ## Hierarchy * **IJsonPatch** ↳ [IReversibleJsonPatch](ireversiblejsonpatch.md) ## Index ### Properties * [op](ijsonpatch.md#op) * [path](ijsonpatch.md#path) * [value](ijsonpatch.md#optional-value) ## Properties ### op • **op**: *"replace" | "add" | "remove"* *Defined in [src/core/json-patch.ts:8](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L8)* ___ ### path • **path**: *string* *Defined in [src/core/json-patch.ts:9](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L9)* ___ ### `Optional` value • **value**? : *any* *Defined in [src/core/json-patch.ts:10](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L10)* ================================================ FILE: docs/API/interfaces/imiddlewareevent.md ================================================ --- id: "imiddlewareevent" title: "IMiddlewareEvent" sidebar_label: "IMiddlewareEvent" --- [mobx-state-tree - v7.0.2](../index.md) › [IMiddlewareEvent](imiddlewareevent.md) ## Hierarchy * [IActionContext](iactioncontext.md) ↳ **IMiddlewareEvent** ## Index ### Properties * [allParentIds](imiddlewareevent.md#allparentids) * [args](imiddlewareevent.md#args) * [context](imiddlewareevent.md#context) * [id](imiddlewareevent.md#id) * [name](imiddlewareevent.md#name) * [parentActionEvent](imiddlewareevent.md#parentactionevent) * [parentEvent](imiddlewareevent.md#parentevent) * [parentId](imiddlewareevent.md#parentid) * [rootId](imiddlewareevent.md#rootid) * [tree](imiddlewareevent.md#tree) * [type](imiddlewareevent.md#type) ## Properties ### allParentIds • **allParentIds**: *number[]* *Defined in [src/core/action.ts:37](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L37)* Id of all events, from root until current (excluding current) ___ ### args • **args**: *any[]* *Inherited from [IActionContext](iactioncontext.md).[args](iactioncontext.md#args)* *Defined in [src/core/actionContext.ts:20](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L20)* Event arguments in an array (action arguments for actions) ___ ### context • **context**: *IAnyStateTreeNode* *Inherited from [IActionContext](iactioncontext.md).[context](iactioncontext.md#context)* *Defined in [src/core/actionContext.ts:15](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L15)* Event context (node where the action was invoked) ___ ### id • **id**: *number* *Inherited from [IActionContext](iactioncontext.md).[id](iactioncontext.md#id)* *Defined in [src/core/actionContext.ts:9](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L9)* Event unique id ___ ### name • **name**: *string* *Inherited from [IActionContext](iactioncontext.md).[name](iactioncontext.md#name)* *Defined in [src/core/actionContext.ts:6](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L6)* Event name (action name for actions) ___ ### parentActionEvent • **parentActionEvent**: *[IMiddlewareEvent](imiddlewareevent.md) | undefined* *Inherited from [IActionContext](iactioncontext.md).[parentActionEvent](iactioncontext.md#parentactionevent)* *Defined in [src/core/actionContext.ts:12](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L12)* Parent action event object ___ ### parentEvent • **parentEvent**: *[IMiddlewareEvent](imiddlewareevent.md) | undefined* *Defined in [src/core/action.ts:32](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L32)* Parent event object ___ ### parentId • **parentId**: *number* *Defined in [src/core/action.ts:30](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L30)* Parent event unique id ___ ### rootId • **rootId**: *number* *Defined in [src/core/action.ts:35](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L35)* Root event unique id ___ ### tree • **tree**: *IAnyStateTreeNode* *Inherited from [IActionContext](iactioncontext.md).[tree](iactioncontext.md#tree)* *Defined in [src/core/actionContext.ts:17](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/actionContext.ts#L17)* Event tree (root node of the node where the action was invoked) ___ ### type • **type**: *[IMiddlewareEventType](../index.md#imiddlewareeventtype)* *Defined in [src/core/action.ts:27](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/action.ts#L27)* Event type ================================================ FILE: docs/API/interfaces/imodelreflectiondata.md ================================================ --- id: "imodelreflectiondata" title: "IModelReflectionData" sidebar_label: "IModelReflectionData" --- [mobx-state-tree - v7.0.2](../index.md) › [IModelReflectionData](imodelreflectiondata.md) ## Hierarchy * [IModelReflectionPropertiesData](imodelreflectionpropertiesdata.md) ↳ **IModelReflectionData** ## Index ### Properties * [actions](imodelreflectiondata.md#actions) * [flowActions](imodelreflectiondata.md#flowactions) * [name](imodelreflectiondata.md#name) * [properties](imodelreflectiondata.md#properties) * [views](imodelreflectiondata.md#views) * [volatile](imodelreflectiondata.md#volatile) ## Properties ### actions • **actions**: *string[]* *Defined in [src/core/mst-operations.ts:855](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L855)* ___ ### flowActions • **flowActions**: *string[]* *Defined in [src/core/mst-operations.ts:858](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L858)* ___ ### name • **name**: *string* *Inherited from [IModelReflectionPropertiesData](imodelreflectionpropertiesdata.md).[name](imodelreflectionpropertiesdata.md#name)* *Defined in [src/core/mst-operations.ts:825](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L825)* ___ ### properties • **properties**: *object* *Inherited from [IModelReflectionPropertiesData](imodelreflectionpropertiesdata.md).[properties](imodelreflectionpropertiesdata.md#properties)* *Defined in [src/core/mst-operations.ts:826](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L826)* #### Type declaration: * \[ **K**: *string*\]: [IAnyType](ianytype.md) ___ ### views • **views**: *string[]* *Defined in [src/core/mst-operations.ts:856](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L856)* ___ ### volatile • **volatile**: *string[]* *Defined in [src/core/mst-operations.ts:857](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L857)* ================================================ FILE: docs/API/interfaces/imodelreflectionpropertiesdata.md ================================================ --- id: "imodelreflectionpropertiesdata" title: "IModelReflectionPropertiesData" sidebar_label: "IModelReflectionPropertiesData" --- [mobx-state-tree - v7.0.2](../index.md) › [IModelReflectionPropertiesData](imodelreflectionpropertiesdata.md) ## Hierarchy * **IModelReflectionPropertiesData** ↳ [IModelReflectionData](imodelreflectiondata.md) ## Index ### Properties * [name](imodelreflectionpropertiesdata.md#name) * [properties](imodelreflectionpropertiesdata.md#properties) ## Properties ### name • **name**: *string* *Defined in [src/core/mst-operations.ts:825](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L825)* ___ ### properties • **properties**: *object* *Defined in [src/core/mst-operations.ts:826](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L826)* #### Type declaration: * \[ **K**: *string*\]: [IAnyType](ianytype.md) ================================================ FILE: docs/API/interfaces/imodeltype.md ================================================ --- id: "imodeltype" title: "IModelType" sidebar_label: "IModelType" --- [mobx-state-tree - v7.0.2](../index.md) › [IModelType](imodeltype.md) ## Type parameters ▪ **PROPS**: *ModelProperties* ▪ **OTHERS** ▪ **CustomC** ▪ **CustomS** ## Hierarchy * [IType](itype.md)‹ModelCreationType2‹PROPS, CustomC›, ModelSnapshotType2‹PROPS, CustomS›, ModelInstanceType‹PROPS, OTHERS›› ↳ **IModelType** ↳ [IAnyModelType](ianymodeltype.md) ## Index ### Properties * [identifierAttribute](imodeltype.md#optional-identifierattribute) * [name](imodeltype.md#name) * [properties](imodeltype.md#properties) ### Methods * [actions](imodeltype.md#actions) * [create](imodeltype.md#create) * [describe](imodeltype.md#describe) * [extend](imodeltype.md#extend) * [is](imodeltype.md#is) * [named](imodeltype.md#named) * [postProcessSnapshot](imodeltype.md#postprocesssnapshot) * [preProcessSnapshot](imodeltype.md#preprocesssnapshot) * [props](imodeltype.md#props) * [validate](imodeltype.md#validate) * [views](imodeltype.md#views) * [volatile](imodeltype.md#volatile) ## Properties ### `Optional` identifierAttribute • **identifierAttribute**? : *undefined | string* *Inherited from [IType](itype.md).[identifierAttribute](itype.md#optional-identifierattribute)* *Defined in [src/core/type/type.ts:92](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L92)* Name of the identifier attribute or null if none. ___ ### name • **name**: *string* *Inherited from [IType](itype.md).[name](itype.md#name)* *Defined in [src/core/type/type.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L87)* Friendly type name. ___ ### properties • **properties**: *PROPS* *Defined in [src/types/complex-types/model.ts:195](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L195)* ## Methods ### actions ▸ **actions**<**A**>(`fn`: function): *[IModelType](imodeltype.md)‹PROPS, OTHERS & A, CustomC, CustomS›* *Defined in [src/types/complex-types/model.ts:209](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L209)* **Type parameters:** ▪ **A**: *ModelActions* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *A* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹PROPS, OTHERS & A, CustomC, CustomS›* ___ ### create ▸ **create**(`snapshot?`: ModelCreationType2‹PROPS, CustomC› | ExcludeReadonly‹ModelInstanceType‹PROPS, OTHERS››, `env?`: any): *this["Type"]* *Inherited from [IType](itype.md).[create](itype.md#create)* *Defined in [src/core/type/type.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L99)* Creates an instance for the type given an snapshot input. **Parameters:** Name | Type | ------ | ------ | `snapshot?` | ModelCreationType2‹PROPS, CustomC› | ExcludeReadonly‹ModelInstanceType‹PROPS, OTHERS›› | `env?` | any | **Returns:** *this["Type"]* An instance of that type. ___ ### describe ▸ **describe**(): *string* *Inherited from [IType](itype.md).[describe](itype.md#describe)* *Defined in [src/core/type/type.ts:121](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L121)* Gets the textual representation of the type as a string. **Returns:** *string* ___ ### extend ▸ **extend**<**A**, **V**, **VS**>(`fn`: function): *[IModelType](imodeltype.md)‹PROPS, OTHERS & A & V & VS, CustomC, CustomS›* *Defined in [src/types/complex-types/model.ts:217](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L217)* **Type parameters:** ▪ **A**: *ModelActions* ▪ **V**: *Object* ▪ **VS**: *Object* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *object* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹PROPS, OTHERS & A & V & VS, CustomC, CustomS›* ___ ### is ▸ **is**(`thing`: any): *thing is ModelCreationType2 | this["Type"]* *Inherited from [IType](itype.md).[is](itype.md#is)* *Defined in [src/core/type/type.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L107)* Checks if a given snapshot / instance is of the given type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | Snapshot or instance to be checked. | **Returns:** *thing is ModelCreationType2 | this["Type"]* true if the value is of the current type, false otherwise. ___ ### named ▸ **named**(`newName`: string): *[IModelType](imodeltype.md)‹PROPS, OTHERS, CustomC, CustomS›* *Defined in [src/types/complex-types/model.ts:197](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L197)* **Parameters:** Name | Type | ------ | ------ | `newName` | string | **Returns:** *[IModelType](imodeltype.md)‹PROPS, OTHERS, CustomC, CustomS›* ___ ### postProcessSnapshot ▸ **postProcessSnapshot**<**NewS**>(`fn`: function): *[IModelType](imodeltype.md)‹PROPS, OTHERS, CustomC, NewS›* *Defined in [src/types/complex-types/model.ts:225](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L225)* **Type parameters:** ▪ **NewS** **Parameters:** ▪ **fn**: *function* ▸ (`snapshot`: ModelSnapshotType2‹PROPS, CustomS›): *NewS* **Parameters:** Name | Type | ------ | ------ | `snapshot` | ModelSnapshotType2‹PROPS, CustomS› | **Returns:** *[IModelType](imodeltype.md)‹PROPS, OTHERS, CustomC, NewS›* ___ ### preProcessSnapshot ▸ **preProcessSnapshot**<**NewC**>(`fn`: function): *[IModelType](imodeltype.md)‹PROPS, OTHERS, NewC, CustomS›* *Defined in [src/types/complex-types/model.ts:221](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L221)* **Type parameters:** ▪ **NewC** **Parameters:** ▪ **fn**: *function* ▸ (`snapshot`: NewC): *WithAdditionalProperties‹ModelCreationType2‹PROPS, CustomC››* **Parameters:** Name | Type | ------ | ------ | `snapshot` | NewC | **Returns:** *[IModelType](imodeltype.md)‹PROPS, OTHERS, NewC, CustomS›* ___ ### props ▸ **props**<**PROPS2**>(`props`: PROPS2): *[IModelType](imodeltype.md)‹PROPS & ModelPropertiesDeclarationToProperties‹PROPS2›, OTHERS, CustomC, CustomS›* *Defined in [src/types/complex-types/model.ts:201](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L201)* **Type parameters:** ▪ **PROPS2**: *ModelPropertiesDeclaration* **Parameters:** Name | Type | ------ | ------ | `props` | PROPS2 | **Returns:** *[IModelType](imodeltype.md)‹PROPS & ModelPropertiesDeclarationToProperties‹PROPS2›, OTHERS, CustomC, CustomS›* ___ ### validate ▸ **validate**(`thing`: ModelCreationType2‹PROPS, CustomC› | ModelInstanceType‹PROPS, OTHERS›, `context`: [IValidationContext](../index.md#ivalidationcontext)): *[IValidationResult](../index.md#ivalidationresult)* *Inherited from [IType](itype.md).[validate](itype.md#validate)* *Defined in [src/core/type/type.ts:116](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L116)* Run's the type's typechecker on the given value with the given validation context. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | ModelCreationType2‹PROPS, CustomC› | ModelInstanceType‹PROPS, OTHERS› | Value to be checked, either a snapshot or an instance. | `context` | [IValidationContext](../index.md#ivalidationcontext) | Validation context, an array of { subpaths, subtypes } that should be validated | **Returns:** *[IValidationResult](../index.md#ivalidationresult)* The validation result, an array with the list of validation errors. ___ ### views ▸ **views**<**V**>(`fn`: function): *[IModelType](imodeltype.md)‹PROPS, OTHERS & V, CustomC, CustomS›* *Defined in [src/types/complex-types/model.ts:205](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L205)* **Type parameters:** ▪ **V**: *Object* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *V* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹PROPS, OTHERS & V, CustomC, CustomS›* ___ ### volatile ▸ **volatile**<**TP**>(`fn`: function): *[IModelType](imodeltype.md)‹PROPS, OTHERS & TP, CustomC, CustomS›* *Defined in [src/types/complex-types/model.ts:213](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/complex-types/model.ts#L213)* **Type parameters:** ▪ **TP**: *object* **Parameters:** ▪ **fn**: *function* ▸ (`self`: [Instance](../index.md#instance)‹this›): *TP* **Parameters:** Name | Type | ------ | ------ | `self` | [Instance](../index.md#instance)‹this› | **Returns:** *[IModelType](imodeltype.md)‹PROPS, OTHERS & TP, CustomC, CustomS›* ================================================ FILE: docs/API/interfaces/ipatchrecorder.md ================================================ --- id: "ipatchrecorder" title: "IPatchRecorder" sidebar_label: "IPatchRecorder" --- [mobx-state-tree - v7.0.2](../index.md) › [IPatchRecorder](ipatchrecorder.md) ## Hierarchy * **IPatchRecorder** ## Index ### Properties * [inversePatches](ipatchrecorder.md#inversepatches) * [patches](ipatchrecorder.md#patches) * [recording](ipatchrecorder.md#recording) * [reversedInversePatches](ipatchrecorder.md#reversedinversepatches) ### Methods * [replay](ipatchrecorder.md#replay) * [resume](ipatchrecorder.md#resume) * [stop](ipatchrecorder.md#stop) * [undo](ipatchrecorder.md#undo) ## Properties ### inversePatches • **inversePatches**: *ReadonlyArray‹[IJsonPatch](ijsonpatch.md)›* *Defined in [src/core/mst-operations.ts:137](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L137)* ___ ### patches • **patches**: *ReadonlyArray‹[IJsonPatch](ijsonpatch.md)›* *Defined in [src/core/mst-operations.ts:136](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L136)* ___ ### recording • **recording**: *boolean* *Defined in [src/core/mst-operations.ts:139](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L139)* ___ ### reversedInversePatches • **reversedInversePatches**: *ReadonlyArray‹[IJsonPatch](ijsonpatch.md)›* *Defined in [src/core/mst-operations.ts:138](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L138)* ## Methods ### replay ▸ **replay**(`target?`: IAnyStateTreeNode): *void* *Defined in [src/core/mst-operations.ts:142](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L142)* **Parameters:** Name | Type | ------ | ------ | `target?` | IAnyStateTreeNode | **Returns:** *void* ___ ### resume ▸ **resume**(): *void* *Defined in [src/core/mst-operations.ts:141](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L141)* **Returns:** *void* ___ ### stop ▸ **stop**(): *void* *Defined in [src/core/mst-operations.ts:140](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L140)* **Returns:** *void* ___ ### undo ▸ **undo**(`target?`: IAnyStateTreeNode): *void* *Defined in [src/core/mst-operations.ts:143](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/mst-operations.ts#L143)* **Parameters:** Name | Type | ------ | ------ | `target?` | IAnyStateTreeNode | **Returns:** *void* ================================================ FILE: docs/API/interfaces/ireversiblejsonpatch.md ================================================ --- id: "ireversiblejsonpatch" title: "IReversibleJsonPatch" sidebar_label: "IReversibleJsonPatch" --- [mobx-state-tree - v7.0.2](../index.md) › [IReversibleJsonPatch](ireversiblejsonpatch.md) ## Hierarchy * [IJsonPatch](ijsonpatch.md) ↳ **IReversibleJsonPatch** ## Index ### Properties * [oldValue](ireversiblejsonpatch.md#oldvalue) * [op](ireversiblejsonpatch.md#op) * [path](ireversiblejsonpatch.md#path) * [value](ireversiblejsonpatch.md#optional-value) ## Properties ### oldValue • **oldValue**: *any* *Defined in [src/core/json-patch.ts:14](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L14)* ___ ### op • **op**: *"replace" | "add" | "remove"* *Inherited from [IJsonPatch](ijsonpatch.md).[op](ijsonpatch.md#op)* *Defined in [src/core/json-patch.ts:8](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L8)* ___ ### path • **path**: *string* *Inherited from [IJsonPatch](ijsonpatch.md).[path](ijsonpatch.md#path)* *Defined in [src/core/json-patch.ts:9](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L9)* ___ ### `Optional` value • **value**? : *any* *Inherited from [IJsonPatch](ijsonpatch.md).[value](ijsonpatch.md#optional-value)* *Defined in [src/core/json-patch.ts:10](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/json-patch.ts#L10)* ================================================ FILE: docs/API/interfaces/iserializedactioncall.md ================================================ --- id: "iserializedactioncall" title: "ISerializedActionCall" sidebar_label: "ISerializedActionCall" --- [mobx-state-tree - v7.0.2](../index.md) › [ISerializedActionCall](iserializedactioncall.md) ## Hierarchy * **ISerializedActionCall** ## Index ### Properties * [args](iserializedactioncall.md#optional-args) * [name](iserializedactioncall.md#name) * [path](iserializedactioncall.md#optional-path) ## Properties ### `Optional` args • **args**? : *any[]* *Defined in [src/middlewares/on-action.ts:33](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L33)* ___ ### name • **name**: *string* *Defined in [src/middlewares/on-action.ts:31](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L31)* ___ ### `Optional` path • **path**? : *undefined | string* *Defined in [src/middlewares/on-action.ts:32](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/middlewares/on-action.ts#L32)* ================================================ FILE: docs/API/interfaces/isimpletype.md ================================================ --- id: "isimpletype" title: "ISimpleType" sidebar_label: "ISimpleType" --- [mobx-state-tree - v7.0.2](../index.md) › [ISimpleType](isimpletype.md) A simple type, this is, a type where the instance and the snapshot representation are the same. ## Type parameters ▪ **T** ## Hierarchy * [IType](itype.md)‹T, T, T› ↳ **ISimpleType** ## Index ### Properties * [identifierAttribute](isimpletype.md#optional-identifierattribute) * [name](isimpletype.md#name) ### Methods * [create](isimpletype.md#create) * [describe](isimpletype.md#describe) * [is](isimpletype.md#is) * [validate](isimpletype.md#validate) ## Properties ### `Optional` identifierAttribute • **identifierAttribute**? : *undefined | string* *Inherited from [IType](itype.md).[identifierAttribute](itype.md#optional-identifierattribute)* *Defined in [src/core/type/type.ts:92](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L92)* Name of the identifier attribute or null if none. ___ ### name • **name**: *string* *Inherited from [IType](itype.md).[name](itype.md#name)* *Defined in [src/core/type/type.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L87)* Friendly type name. ## Methods ### create ▸ **create**(`snapshot?`: T | ExcludeReadonly‹T›, `env?`: any): *this["Type"]* *Inherited from [IType](itype.md).[create](itype.md#create)* *Defined in [src/core/type/type.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L99)* Creates an instance for the type given an snapshot input. **Parameters:** Name | Type | ------ | ------ | `snapshot?` | T | ExcludeReadonly‹T› | `env?` | any | **Returns:** *this["Type"]* An instance of that type. ___ ### describe ▸ **describe**(): *string* *Inherited from [IType](itype.md).[describe](itype.md#describe)* *Defined in [src/core/type/type.ts:121](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L121)* Gets the textual representation of the type as a string. **Returns:** *string* ___ ### is ▸ **is**(`thing`: any): *thing is T | this["Type"]* *Inherited from [IType](itype.md).[is](itype.md#is)* *Defined in [src/core/type/type.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L107)* Checks if a given snapshot / instance is of the given type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | Snapshot or instance to be checked. | **Returns:** *thing is T | this["Type"]* true if the value is of the current type, false otherwise. ___ ### validate ▸ **validate**(`thing`: T | T, `context`: [IValidationContext](../index.md#ivalidationcontext)): *[IValidationResult](../index.md#ivalidationresult)* *Inherited from [IType](itype.md).[validate](itype.md#validate)* *Defined in [src/core/type/type.ts:116](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L116)* Run's the type's typechecker on the given value with the given validation context. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | T | T | Value to be checked, either a snapshot or an instance. | `context` | [IValidationContext](../index.md#ivalidationcontext) | Validation context, an array of { subpaths, subtypes } that should be validated | **Returns:** *[IValidationResult](../index.md#ivalidationresult)* The validation result, an array with the list of validation errors. ================================================ FILE: docs/API/interfaces/isnapshotprocessor.md ================================================ --- id: "isnapshotprocessor" title: "ISnapshotProcessor" sidebar_label: "ISnapshotProcessor" --- [mobx-state-tree - v7.0.2](../index.md) › [ISnapshotProcessor](isnapshotprocessor.md) A type that has its snapshots processed. ## Type parameters ▪ **IT**: *[IAnyType](ianytype.md)* ▪ **CustomC** ▪ **CustomS** ## Hierarchy * [IType](itype.md)‹_CustomOrOther‹CustomC, IT["CreationType"]›, _CustomOrOther‹CustomS, IT["SnapshotType"]›, IT["TypeWithoutSTN"]› ↳ **ISnapshotProcessor** ## Index ### Properties * [identifierAttribute](isnapshotprocessor.md#optional-identifierattribute) * [name](isnapshotprocessor.md#name) ### Methods * [create](isnapshotprocessor.md#create) * [describe](isnapshotprocessor.md#describe) * [is](isnapshotprocessor.md#is) * [validate](isnapshotprocessor.md#validate) ## Properties ### `Optional` identifierAttribute • **identifierAttribute**? : *undefined | string* *Inherited from [IType](itype.md).[identifierAttribute](itype.md#optional-identifierattribute)* *Defined in [src/core/type/type.ts:92](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L92)* Name of the identifier attribute or null if none. ___ ### name • **name**: *string* *Inherited from [IType](itype.md).[name](itype.md#name)* *Defined in [src/core/type/type.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L87)* Friendly type name. ## Methods ### create ▸ **create**(`snapshot?`: _CustomOrOther‹CustomC, IT["CreationType"]› | ExcludeReadonly‹IT["TypeWithoutSTN"]›, `env?`: any): *this["Type"]* *Inherited from [IType](itype.md).[create](itype.md#create)* *Defined in [src/core/type/type.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L99)* Creates an instance for the type given an snapshot input. **Parameters:** Name | Type | ------ | ------ | `snapshot?` | _CustomOrOther‹CustomC, IT["CreationType"]› | ExcludeReadonly‹IT["TypeWithoutSTN"]› | `env?` | any | **Returns:** *this["Type"]* An instance of that type. ___ ### describe ▸ **describe**(): *string* *Inherited from [IType](itype.md).[describe](itype.md#describe)* *Defined in [src/core/type/type.ts:121](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L121)* Gets the textual representation of the type as a string. **Returns:** *string* ___ ### is ▸ **is**(`thing`: any): *thing is _CustomOrOther | this["Type"]* *Inherited from [IType](itype.md).[is](itype.md#is)* *Defined in [src/core/type/type.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L107)* Checks if a given snapshot / instance is of the given type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | Snapshot or instance to be checked. | **Returns:** *thing is _CustomOrOther | this["Type"]* true if the value is of the current type, false otherwise. ___ ### validate ▸ **validate**(`thing`: _CustomOrOther‹CustomC, IT["CreationType"]› | IT["TypeWithoutSTN"], `context`: [IValidationContext](../index.md#ivalidationcontext)): *[IValidationResult](../index.md#ivalidationresult)* *Inherited from [IType](itype.md).[validate](itype.md#validate)* *Defined in [src/core/type/type.ts:116](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L116)* Run's the type's typechecker on the given value with the given validation context. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | _CustomOrOther‹CustomC, IT["CreationType"]› | IT["TypeWithoutSTN"] | Value to be checked, either a snapshot or an instance. | `context` | [IValidationContext](../index.md#ivalidationcontext) | Validation context, an array of { subpaths, subtypes } that should be validated | **Returns:** *[IValidationResult](../index.md#ivalidationresult)* The validation result, an array with the list of validation errors. ================================================ FILE: docs/API/interfaces/isnapshotprocessors.md ================================================ --- id: "isnapshotprocessors" title: "ISnapshotProcessors" sidebar_label: "ISnapshotProcessors" --- [mobx-state-tree - v7.0.2](../index.md) › [ISnapshotProcessors](isnapshotprocessors.md) Snapshot processors. ## Type parameters ▪ **IT**: *[IAnyType](ianytype.md)* ▪ **CustomC** ▪ **CustomS** ## Hierarchy * **ISnapshotProcessors** ## Index ### Methods * [postProcessor](isnapshotprocessors.md#optional-postprocessor) * [preProcessor](isnapshotprocessors.md#optional-preprocessor) ## Methods ### `Optional` postProcessor ▸ **postProcessor**(`snapshot`: IT["SnapshotType"], `node`: [Instance](../index.md#instance)‹IT›): *_CustomOrOther‹CustomS, IT["SnapshotType"]›* *Defined in [src/types/utility-types/snapshotProcessor.ts:230](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/snapshotProcessor.ts#L230)* Function that transforms an output snapshot. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `snapshot` | IT["SnapshotType"] | | `node` | [Instance](../index.md#instance)‹IT› | - | **Returns:** *_CustomOrOther‹CustomS, IT["SnapshotType"]›* ___ ### `Optional` preProcessor ▸ **preProcessor**(`snapshot`: _CustomOrOther‹CustomC, IT["CreationType"]›): *IT["CreationType"]* *Defined in [src/types/utility-types/snapshotProcessor.ts:224](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/snapshotProcessor.ts#L224)* Function that transforms an input snapshot. **Parameters:** Name | Type | ------ | ------ | `snapshot` | _CustomOrOther‹CustomC, IT["CreationType"]› | **Returns:** *IT["CreationType"]* ================================================ FILE: docs/API/interfaces/itype.md ================================================ --- id: "itype" title: "IType" sidebar_label: "IType" --- [mobx-state-tree - v7.0.2](../index.md) › [IType](itype.md) A type, either complex or simple. ## Type parameters ▪ **C** ▪ **S** ▪ **T** ## Hierarchy * **IType** ↳ [IAnyType](ianytype.md) ↳ [ISimpleType](isimpletype.md) ↳ [IAnyComplexType](ianycomplextype.md) ↳ [ISnapshotProcessor](isnapshotprocessor.md) ↳ [IModelType](imodeltype.md) ## Index ### Properties * [identifierAttribute](itype.md#optional-identifierattribute) * [name](itype.md#name) ### Methods * [create](itype.md#create) * [describe](itype.md#describe) * [is](itype.md#is) * [validate](itype.md#validate) ## Properties ### `Optional` identifierAttribute • **identifierAttribute**? : *undefined | string* *Defined in [src/core/type/type.ts:92](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L92)* Name of the identifier attribute or null if none. ___ ### name • **name**: *string* *Defined in [src/core/type/type.ts:87](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L87)* Friendly type name. ## Methods ### create ▸ **create**(`snapshot?`: C | ExcludeReadonly‹T›, `env?`: any): *this["Type"]* *Defined in [src/core/type/type.ts:99](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L99)* Creates an instance for the type given an snapshot input. **Parameters:** Name | Type | ------ | ------ | `snapshot?` | C | ExcludeReadonly‹T› | `env?` | any | **Returns:** *this["Type"]* An instance of that type. ___ ### describe ▸ **describe**(): *string* *Defined in [src/core/type/type.ts:121](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L121)* Gets the textual representation of the type as a string. **Returns:** *string* ___ ### is ▸ **is**(`thing`: any): *thing is C | this["Type"]* *Defined in [src/core/type/type.ts:107](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L107)* Checks if a given snapshot / instance is of the given type. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | any | Snapshot or instance to be checked. | **Returns:** *thing is C | this["Type"]* true if the value is of the current type, false otherwise. ___ ### validate ▸ **validate**(`thing`: C | T, `context`: [IValidationContext](../index.md#ivalidationcontext)): *[IValidationResult](../index.md#ivalidationresult)* *Defined in [src/core/type/type.ts:116](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type.ts#L116)* Run's the type's typechecker on the given value with the given validation context. **Parameters:** Name | Type | Description | ------ | ------ | ------ | `thing` | C | T | Value to be checked, either a snapshot or an instance. | `context` | [IValidationContext](../index.md#ivalidationcontext) | Validation context, an array of { subpaths, subtypes } that should be validated | **Returns:** *[IValidationResult](../index.md#ivalidationresult)* The validation result, an array with the list of validation errors. ================================================ FILE: docs/API/interfaces/ivalidationcontextentry.md ================================================ --- id: "ivalidationcontextentry" title: "IValidationContextEntry" sidebar_label: "IValidationContextEntry" --- [mobx-state-tree - v7.0.2](../index.md) › [IValidationContextEntry](ivalidationcontextentry.md) Validation context entry, this is, where the validation should run against which type ## Hierarchy * **IValidationContextEntry** ## Index ### Properties * [path](ivalidationcontextentry.md#path) * [type](ivalidationcontextentry.md#type) ## Properties ### path • **path**: *string* *Defined in [src/core/type/type-checker.ts:17](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L17)* Subpath where the validation should be run, or an empty string to validate it all ___ ### type • **type**: *[IAnyType](ianytype.md)* *Defined in [src/core/type/type-checker.ts:19](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L19)* Type to validate the subpath against ================================================ FILE: docs/API/interfaces/ivalidationerror.md ================================================ --- id: "ivalidationerror" title: "IValidationError" sidebar_label: "IValidationError" --- [mobx-state-tree - v7.0.2](../index.md) › [IValidationError](ivalidationerror.md) Type validation error ## Hierarchy * **IValidationError** ## Index ### Properties * [context](ivalidationerror.md#context) * [message](ivalidationerror.md#optional-message) * [value](ivalidationerror.md#value) ## Properties ### context • **context**: *[IValidationContext](../index.md#ivalidationcontext)* *Defined in [src/core/type/type-checker.ts:28](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L28)* Validation context ___ ### `Optional` message • **message**? : *undefined | string* *Defined in [src/core/type/type-checker.ts:32](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L32)* Error message ___ ### value • **value**: *any* *Defined in [src/core/type/type-checker.ts:30](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/core/type/type-checker.ts#L30)* Value that was being validated, either a snapshot or an instance ================================================ FILE: docs/API/interfaces/referenceoptionsgetset.md ================================================ --- id: "referenceoptionsgetset" title: "ReferenceOptionsGetSet" sidebar_label: "ReferenceOptionsGetSet" --- [mobx-state-tree - v7.0.2](../index.md) › [ReferenceOptionsGetSet](referenceoptionsgetset.md) ## Type parameters ▪ **IT**: *[IAnyComplexType](ianycomplextype.md)* ## Hierarchy * **ReferenceOptionsGetSet** ## Index ### Methods * [get](referenceoptionsgetset.md#get) * [set](referenceoptionsgetset.md#set) ## Methods ### get ▸ **get**(`identifier`: [ReferenceIdentifier](../index.md#referenceidentifier), `parent`: IAnyStateTreeNode | null): *ReferenceT‹IT›* *Defined in [src/types/utility-types/reference.ts:472](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L472)* **Parameters:** Name | Type | ------ | ------ | `identifier` | [ReferenceIdentifier](../index.md#referenceidentifier) | `parent` | IAnyStateTreeNode | null | **Returns:** *ReferenceT‹IT›* ___ ### set ▸ **set**(`value`: ReferenceT‹IT›, `parent`: IAnyStateTreeNode | null): *[ReferenceIdentifier](../index.md#referenceidentifier)* *Defined in [src/types/utility-types/reference.ts:473](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L473)* **Parameters:** Name | Type | ------ | ------ | `value` | ReferenceT‹IT› | `parent` | IAnyStateTreeNode | null | **Returns:** *[ReferenceIdentifier](../index.md#referenceidentifier)* ================================================ FILE: docs/API/interfaces/referenceoptionsoninvalidated.md ================================================ --- id: "referenceoptionsoninvalidated" title: "ReferenceOptionsOnInvalidated" sidebar_label: "ReferenceOptionsOnInvalidated" --- [mobx-state-tree - v7.0.2](../index.md) › [ReferenceOptionsOnInvalidated](referenceoptionsoninvalidated.md) ## Type parameters ▪ **IT**: *[IAnyComplexType](ianycomplextype.md)* ## Hierarchy * **ReferenceOptionsOnInvalidated** ## Index ### Properties * [onInvalidated](referenceoptionsoninvalidated.md#oninvalidated) ## Properties ### onInvalidated • **onInvalidated**: *[OnReferenceInvalidated](../index.md#onreferenceinvalidated)‹ReferenceT‹IT››* *Defined in [src/types/utility-types/reference.ts:478](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/reference.ts#L478)* ================================================ FILE: docs/API/interfaces/unionoptions.md ================================================ --- id: "unionoptions" title: "UnionOptions" sidebar_label: "UnionOptions" --- [mobx-state-tree - v7.0.2](../index.md) › [UnionOptions](unionoptions.md) ## Type parameters ▪ **Types**: *[IAnyType](ianytype.md)[]* ## Hierarchy * **UnionOptions** ## Index ### Properties * [dispatcher](unionoptions.md#optional-dispatcher) * [eager](unionoptions.md#optional-eager) ## Properties ### `Optional` dispatcher • **dispatcher**? : *[ITypeDispatcher](../index.md#itypedispatcher)‹Types›* *Defined in [src/types/utility-types/union.ts:38](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L38)* A function that returns the type to be used given an input snapshot. ___ ### `Optional` eager • **eager**? : *undefined | false | true* *Defined in [src/types/utility-types/union.ts:33](https://github.com/mobxjs/mobx-state-tree/blob/1be40a3e/src/types/utility-types/union.ts#L33)* Whether or not to use eager validation. When `true`, the first matching type will be used. Otherwise, all types will be checked and the validation will pass if and only if a single type matches. ================================================ FILE: docs/API_header.md ================================================ # Mobx-State-Tree API reference guide _This reference guide lists all methods exposed by MST. Contributions like linguistic improvements, adding more details to the descriptions or additional examples are highly appreciated! Please note that the docs are generated from source. Most methods are declared in the [mst-operations.ts](https://github.com/mobxjs/mobx-state-tree/blob/master/packages/mobx-state-tree/src/core/mst-operations.ts) file._ ================================================ FILE: docs/compare/context-reducer-vs-mobx-state-tree.md ================================================ --- id: context-reducer-vs-mobx-state-tree title: React Context vs. MobX-State-Tree --- If you're using React, you have the option to manage application state with built in hooks, like [`useContext`](https://react.dev/reference/react/useContext) and [`useReducer`](https://react.dev/reference/react/useReducer). The React docs have [an example showing how to combine these two hooks to manage more complex state](https://react.dev/learn/scaling-up-with-reducer-and-context). React built-ins are a great choice if you're opposed to adding dependencies to your project, or if you want to write flexible JavaScript code with your own set of conventions. MobX-State-Tree can provide you with the same features as React's built-in state management hooks, but with the added benefits of: - Better performance out of the box due to MST's reactive, observable state. - Automatic TypeScript inference of your state, which makes your code easier to write (with auto-completions) and harder to break (with static analysis of TypeScript). - Runtime type safety for your state, which also helps keep your application bug free as your codebase and team grows. - Clearer data modeling with our rich runtime type system as opposed to writing plain JS objects. - Built-in immutability with [snapshots](../concepts/snapshots.md). This makes it easy to build common requirements like "undo/redo", time travel debugging, or synchronizing with external systems - Easy persistence with utilities like [mst-persist](https://www.npmjs.com/package/mst-persist) ## React Context/Reducer Code Review If you haven't worked with complex contexts and reducers in React, you should read through [their guide on advanced usage](https://react.dev/learn/scaling-up-with-reducer-and-context). It will help you make a fair assessment between React state hooks and MobX-State-Tree. [Here is the CodeSandbox of their final product in that article](https://codesandbox.io/p/sandbox/react-dev-wy7lfd?file=%2Fsrc%2FTasksContext.js%3A54%2C4&utm_medium=sandpack). [And here is the same set of features, built with MobX-State-Tree instead of Context/Reducers](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-8824l8?file=%2Fsrc%2FViewModel.ts%3A15%2C24.). Let's focus on comparing just the state-management code in React's `src/TasksContext.js`, and MST's `src/ViewModel.ts`. To start, we'll compare code, and then we'll move on to feature comparisons. ```js // React context/reducer in `src/TasksContext.js` // https://codesandbox.io/p/sandbox/react-dev-wy7lfd?file=%2Fsrc%2FTasksContext.js%3A43%2C17&utm_medium=sandpack import { createContext, useContext, useReducer } from "react" const TasksContext = createContext(null) const TasksDispatchContext = createContext(null) export function TasksProvider({ children }) { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks) return ( {children} ) } export function useTasks() { return useContext(TasksContext) } export function useTasksDispatch() { return useContext(TasksDispatchContext) } function tasksReducer(tasks, action) { switch (action.type) { case "added": { return [ ...tasks, { id: action.id, text: action.text, done: false } ] } case "changed": { return tasks.map((t) => { if (t.id === action.task.id) { return action.task } else { return t } }) } case "deleted": { return tasks.filter((t) => t.id !== action.id) } default: { throw Error("Unknown action: " + action.type) } } } const initialTasks = [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false } ] ``` ### React Code is Tightly Coupled The Context/Reducer code is, understandably, very coupled to React. It exports JSX directly: ```js export function TasksProvider({ children }) { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks) return ( {children} ) } ``` It also mixes concerns. Note how in `TasksProvider`, the reducer, initial tasks, and dispatch value have to come together with the UI code to become useful. It's not entirely clear from a top-to-bottom glance where the source of truth for state is. ### Reducer Functions Lack Convention Check out the reducer function: ```js function tasksReducer(tasks, action) { switch (action.type) { case "added": { return [ ...tasks, { id: action.id, text: action.text, done: false } ] } case "changed": { return tasks.map((t) => { if (t.id === action.task.id) { return action.task } else { return t } }) } case "deleted": { return tasks.filter((t) => t.id !== action.id) } default: { throw Error("Unknown action: " + action.type) } } } ``` With three actions, this feels somewhat manageable. But what if your state mutations are more numerous or more complex? Of course you can split those out into other files, but then your codebase gets fragmented, and it's becomes more difficult to reason about it overtime. Moreover, the `action` argument is opaque. What types are valid? What other data will come along with it? You could write these out in TypeScript and define valid shapes, but that's more work and boilerplate for you. ### Unclear Initial State in Context/Reducer The reducer/context example provides `initialTasks`, like this: ```js const initialTasks = [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false } ] ``` But those are just the initial tasks. If you followed the React tutorial, you might be wondering: 1. How do we know if an item is being edited? 2. Where are we storing `nextId`? Turns out, the item being edited is managed as local state with `useState` in [`src/TaskList.js`](https://codesandbox.io/p/sandbox/react-dev-wy7lfd?file=%2Fsrc%2FTaskList.js%3A15%2C2&utm_medium=sandpack): ```js // ... function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> ); } else { taskContent = ( <> {task.text} ); } // ... ``` We use an auto-incrementing number for IDs. In the React example, this is stored and initialized in [`src/AddTask.js`](https://codesandbox.io/p/sandbox/react-dev-wy7lfd?file=%2Fsrc%2FAddTask.js%3A27%2C1&utm_medium=sandpack): ```js // At the bottom of `src/AddTask.js`: let nextId = 3 ``` ## MobX-State-Tree Code Review ```ts // MST's viewmodel in `src/ViewModel.ts`. // https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-8824l8?file=%2Fsrc%2FViewModel.ts%3A88%2C1 import { t, Instance } from "mobx-state-tree" const Task = t .model("Task", { id: t.identifierNumber, text: t.string, done: t.optional(t.boolean, false), isBeingEdited: t.optional(t.boolean, false) }) .actions((self) => ({ setText(text: string) { self.text = text }, setDone(done: boolean) { self.done = done }, setIsBeingEdited(beingEdited: boolean) { self.isBeingEdited = beingEdited } })) export interface ITask extends Instance {} const ViewModel = t .model("ViewModel", { taskInputText: "", nextId: 0, tasks: t.array(Task) }) .actions((self) => ({ addTask() { const { nextId, taskInputText } = self if (!taskInputText) { return } const newTask = Task.create({ id: nextId, text: taskInputText }) self.tasks.push(newTask) self.nextId += 1 self.taskInputText = "" }, deleteTask(id: number) { const task = self.tasks.find((t) => t.id === id) if (task) { self.tasks.remove(task) } }, setInputText(text: string) { self.taskInputText = text } })) export const ViewModelSingleton = ViewModel.create({ nextId: 3, tasks: [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false } ] }) ``` ### MobX-State-Tree Decouples State from UI The MobX-State-Tree code doesn't really "know" anything about React (or Vue, or Angular, or Solid, or Svelte, or any other library you might be using). It is Just TypeScript. Which means it does not suffer from the [coupling problems of React state built-ins](#react-code-is-tightly-coupled). We can't really fault React tools for being coupled to React, but using MST will provide you with more flexibility to change your UI code, and even your entire UI library if you ever choose to. ### Conventional State Change with Actions The `.actions` block in our MST code replaces the React reducer. Rather than managing our actions with dispatches and a switch statement, we can write state mutations as regular TypeScript functions. Each aciton gets its own set of parameters. You can call those actions like regular functions, rather than "dispatching" the action boilerplate. This is the code we're talking about: ```ts .actions((self) => ({ addTask() { const { nextId, taskInputText } = self; if (!taskInputText) { return; } const newTask = Task.create({ id: nextId, text: taskInputText, }); self.tasks.push(newTask); self.nextId += 1; self.taskInputText = ""; }, deleteTask(id: number) { const task = self.tasks.find((t) => t.id === id); if (task) { self.tasks.remove(task); } }, setInputText(text: string) { self.taskInputText = text; }, })); ``` If you want to add a task, you'd call: ```ts ViewModelSingleton.addtask() ``` And we'd create a task based on the current state of the `taskInputText`. State would update, and the UI would respond to the granular updates. Simple and lovely to work with! ### MST is a Single Source of Truth for State It's easier to clarify initial state in MobX-State-Tree. In our example, we provide it much like the [initial state in Context](#unclear-initial-state-in-contextreducer): ```ts export const ViewModelSingleton = ViewModel.create({ nextId: 3, tasks: [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false } ] }) ``` This code is creating a new instance of a ViewModel, and it's providing it with all of the initial state we need. If we gave an invalid initial state, MobX-State-Tree would warn us: ```ts export const ViewModelSingleton = ViewModel.create({ nextId: "3", // In this example, we're using numbers for IDs, not strings. TS will error. tasks: [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false } ] }) ``` If we use the wrong kind of value for our `nextId`, we'll get a TypeScript error like: ``` Type 'string' is not assignable to type 'number'.typescript(2322) ``` Even if you're not using TypeScript, MST will let you know about it in the runtime: ``` [mobx-state-tree] Error while converting `{"nextId":"3","tasks":[{"id":0,"text":"Philosopher’s Path","done":true},{"id":1,"text":"Visit the temple","done":false},{"id":2,"text":"Drink matcha","done":false}]}` to `ViewModel`: at path "/nextId" value `"3"` is not assignable to type: `number` (Value is not a number). ``` If you want to avoid even this much initial code, you can initialize the ViewModel with no tasks. Since we wrote the `nextId` value as a literal, MST will assume it's optional, and the provided value is the default. So this code: ```ts const ViewModel = t.model("ViewModel", { taskInputText: t.maybe(t.string), nextId: 0, tasks: t.array(Task) }) ``` Allows us to write: ```ts export const ViewModelSingleton = ViewModel.create({}) ``` It _also_ keeps all of this state in one central place. We can read the file top-to-bottom and understand the entirety of our state at a glance. ## React Context/Reducer Rendering Performance Imagine you want to use React Context in a large React application with many layers of nesting. As a simple demonstration, consider what happens if we wrap our code in some `MiddleComponent`: ```js // src/MiddleComponent.js export default function MiddleComponent(props) { const { children } = props; console.log("MiddleComponent evaluated"); return
{children}
; } // src/App.js import AddTask from "./AddTask.js"; import TaskList from "./TaskList.js"; import MiddleComponent from "./MiddleComponent.js"; import { TasksProvider } from "./TasksContext.js"; export default function TaskApp() { return (

Day off in Kyoto

); } ``` [Play around with this in CodeSandbox and pay attention to the console](https://codesandbox.io/p/sandbox/react-dev-reducer-context-with-middle-component-cjgg72?file=%2Fsrc%2FMiddleComponent.js%3A11%2C1). Add some to-dos, delete some, check some off. You'll see this output in the console: ``` MiddleComponent evaluated MiddleComponent evaluated MiddleComponent evaluated MiddleComponent evaluated MiddleComponent evaluated MiddleComponent evaluated MiddleComponent evaluated MiddleComponent evaluated MiddleComponent evaluated ``` And so on, for as many times as you change the values in the context provider. ### React Makes You Manage Optimization Yourself You can improve this in React with memoization: ```jsx // src/MiddleComponent.js import React from "react" const MiddleComponent = React.memo(function MiddleComponent(props) { const { children } = props console.log("MiddleComponent evaluated") return
{children}
}) export default MiddleComponent ``` Or you can split context into many sub-contexts and provide them to children more granularly. But all that said, _you_ still have to manage this complexity in some way. This is advantageous if you and your team are adept at performance work, and want to have fine-grained control of the primitive building blocks provided by React. But many teams lack the expertise, time, or interest in managing this themselves. MobX-State-Tree solves this performance issue for you by default. ## MobX-State-Tree Performance Given a similar component and setup: ```tsx // src/MiddleComponent.tsx import React from "react" export default function MiddleComponent(props) { const { children } = props console.log("MiddleComponent evaluated") return
{children}
} // src/App.tsx import AddTask from "./AddTask" import TaskList from "./TaskList" import MiddleComponent from "./MiddleComponent" export default function TaskApp() { return ( <>

Day off in Kyoto

) } ``` ### Handles Granular Updates Automatically [Try the same set of actions in CodeSandbox](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-with-middle-component-y2h558?file=%2Fsrc%2FMiddleComponent.tsx%3A9%2C1), and you'll see that the `MiddleComponent` does _not_ get re-evaluated. MobX-State-Tree does this for you with its [observer higher-order-component](../intro/getting-started#getting-to-the-ui), which [only re-renders components when their observed data changes](../intro/getting-started#improving-render-performance). ## Automatic TypeScript Types with MobX-State-Tree So far we've been comparing React's [JavaScript only example](https://codesandbox.io/p/sandbox/react-dev-wy7lfd?file=%2Fsrc%2Findex.js%3A28%2C30&utm_medium=sandpack) against a MobX-State-Tree example [written in TypeScript](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-8824l8?file=%2Fsrc%2FViewModel.ts%3A11%2C22). The TypeScript story for MobX-State-Tree is very straightforward. In [`src/ViewModel.ts`](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-8824l8?file=%2Fsrc%2FViewModel.ts%3A69%2C20) you can write `ViewModelSingleton.` and get auto-complete for all its properties and actions. If you want to do more with these types, we [have a recommended set of type helpers](../tips/typescript#using-a-mst-type-at-design-time). In our example, you can see we use: ```ts export interface ITask extends Instance {} ``` To tell the [Task component](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-8824l8?file=%2Fsrc%2FTaskList.tsx%3A27%2C19) what to expect in its props. You don't have to make any choices about the TypeScript design. Model out your state with MST, and we'll give you an opinionated set of TypeScript types back. You trade off control for quicker development overall, much like the performance management tradeoffs. If you want good, sensible defaults for rapid development, choose MobX-State-Tree. ## Write Your Own Types for React Context/Reducer If you want to use React Context/Reducer with TypeScript, you'll need to specify your types from the ground up. Many teams might like this approach, but it does require you to take the time to do so. Here's one way you might type the context: ```tsx import React, { createContext, useContext, useReducer, ReactNode, Dispatch, JSX } from "react" export interface Task { id: number text: string done: boolean } type Action = | { type: "added"; id: number; text: string } | { type: "changed"; task: Task } | { type: "deleted"; id: number } const TasksContext = createContext(null) const TasksDispatchContext = createContext | null>(null) export function TasksProvider({ children }: { children: ReactNode }): JSX.Element { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks) return ( {children} ) } export function useTasks(): Task[] { const context = useContext(TasksContext) if (!context) { throw new Error("useTasks must be used within a TasksProvider") } return context } export function useTasksDispatch(): Dispatch { const context = useContext(TasksDispatchContext) if (!context) { throw new Error("useTasksDispatch must be used within a TasksProvider") } return context } function tasksReducer(tasks: Task[], action: Action): Task[] { switch (action.type) { case "added": { return [ ...tasks, { id: action.id, text: action.text, done: false } ] } case "changed": { return tasks.map((t) => { if (t.id === action.task.id) { return action.task } else { return t } }) } case "deleted": { return tasks.filter((t) => t.id !== action.id) } default: { throw Error("Unknown action: " + action) } } } const initialTasks = [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false } ] ``` [See the whole example converted to TypeScript in CodeSandbox](https://codesandbox.io/p/sandbox/react-dev-context-reducer-example-with-typescript-l8ym3t?file=%2Fsrc%2FTasksContext.tsx%3A91%2C1) ## Context/Reducer Cannot Guarantee Type Safety at Runtime In the React Context/Reducer example, you are required to understand the kinds of initial data that satisfy your requirements. You must remember how to write them, and write them consistently. The example provides initial tasks like this: ```js const initialTasks = [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false } ] ``` But if you write an invalid task, React won't stop you: ```js const initialTasks = [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false }, { something: "else", works: false, id: () => { console.log("here") } } ] ``` In fact, React _almost_ does the right thing here. If you check out the [CodeSandbox with this incorrect data](https://codesandbox.io/p/sandbox/react-dev-context-reducer-with-incorrect-task-data-nqw67d?file=%2Fsrc%2FTasksContext.js%3A56%2C1), you'll see that a fourth item shows up. You can even edit/delete/check it off. The React components themselves are pretty resilient. But if you check off the task, or edit its name, you'll get a warning in the console: ``` Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components ``` This is because we've left `text` and `done` to be `undefined`, and then the reducer modifies those values. In the small, toy React example, this isn't a huge deal. But this kind of unexpected behavior can lead to serious bugs in a larger application. ## MobX-State-Tree Provides Runtime Type Safety by Default [Open up the MST example in CodeSandbox](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-8824l8?file=%2Fsrc%2FViewModel.ts%3A15%2C24.) and change the ViewModel instantiation to be: ```ts export const ViewModelSingleton = ViewModel.create({ nextId: 3, tasks: [ { id: 0, text: "Philosopher’s Path", done: true }, { id: 1, text: "Visit the temple", done: false }, { id: 2, text: "Drink matcha", done: false }, { something: "else", works: false, id: () => { console.log("here") } } ] }) ``` You'll _immediately receive an error from MobX-State-Tree_: ``` [mobx-state-tree] Error while converting `{"nextId":3,"tasks":[{"id":0,"text":"Philosopher’s Path","done":true},{"id":1,"text":"Visit the temple","done":false},{"id":2,"text":"Drink matcha","done":false},{"something":"else","works":false}]}` to `ViewModel`: at path "/tasks/3/id" snapshot is not assignable to type: `identifierNumber` (Value is not a valid identifierNumber, expected a number), expected an instance of `identifierNumber` or a snapshot like `identifierNumber` instead. at path "/tasks/3/text" value `undefined` is not assignable to type: `string` (Value is not a string). ``` This error will both prevent you from making costly mistakes in the future, and it even attempts to give you information about _precisely what's wrong_, which makes debugging things easier. _(Note: by default, MST will not run this check in production mode for performance reasons)_ ## MobX-State-Tree Gives you Building Blocks for Advanced Data Modeling In the Reducer/Context example, we arbitrarily decide that a task looks like this: ```js { id: 0, text: "Philosopher’s Path", done: true }, ``` With TypeScript, we can annotate the types of these objects. But if you're building a complex app, you may want to enforce your data modeling beyond conventions and static types. In MobX-State-Tree, we turned that object syntax into a model itself: ```ts const Task = t .model("Task", { id: t.identifierNumber, text: t.string, done: t.optional(t.boolean, false), isBeingEdited: t.optional(t.boolean, false) }) .actions((self) => ({ setText(text: string) { self.text = text }, setDone(done: boolean) { self.done = done }, setIsBeingEdited(beingEdited: boolean) { self.isBeingEdited = beingEdited } })) ``` Now our program understands that a `Task` is a real entity with a well-defined set of properties, and well-defined actions it can take at runtime. This is a clearer way to communicate your intention to other programmers, and to enforce rules for your data modeling in your application. There are [many different types](../overview/types.md) you can extend and build with to provide this same kind of structure and safety to your application at all levels. This is another tradeoff: MST primitives and models have rules that plain JavaScript objects do not. But if you learn those rules, you can improve your developer experience, and more rigorously model your application state for your future self and the rest of your team to work with correctly. ## React Context/Reducer Needs Custom Code for Time Travel Debugging [Time travel debugging](https://medium.com/the-web-tub/time-travel-in-react-redux-apps-using-the-redux-devtools-5e94eba5e7c0) is a popular tool used to observe how application state changes over time, and diagnose any errors or inaccuracies. The idea is to keep a record of the state and its mutations over time, and then play it back through some dev tooling or observability that understands how to represent the state. Building this kind of functionality is possible with Reducers and Context, but you have to build it yourself, from the ground up. ## MobX-State-Tree Has Built-in Time Travel Primitives MobX-State-Tree generates [snapshots](../concepts/snapshots.md), which are immutable, serialized versions of the state at each point it gets changes. You can listen to the snapshots with the `onSnapshot` listener, like this: ```ts const initialSnapshot = JSON.stringify(getSnapshot(ViewModelSingleton)) const timeTravel: string[] = [initialSnapshot] onSnapshot(ViewModelSingleton, (snapshot) => { timeTravel.push(JSON.stringify(snapshot)) }) ``` In this code, we take an inital snapshot of the `ViewModelSingleton`, and then store each subsequent snapshot. You can play around with this in [CodeSandbox](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-snapshots-qvr529?file=%2Fsrc%2FViewModel.ts%3A54%2C31). Open up the console, and store the `timeTravel` variable as a global variable. Log it out after you make some changes, and you'll see a series of snapshots. Snapshots like this make time travel debugging easy to implement, with very little custom code. It also makes it easy to do things like persistence, re-hydrating state from the server, and other operations where serialized state can be deserialized into something more useful. The following section is a great example of this. ## Persist State Easily with mst-persist Since MobX-State-Tree state is always serializable and we have utilities like snapshot listeners, libraries like [mst-persist](https://www.npmjs.com/package/mst-persist) are readily available. With one import and one line of code, we can persist our application state to localStorage: ``` import { persist } from "mst-persist"; persist("ViewModelSingleton", ViewModelSingleton) ``` [Open this CodeSandbox example](https://codesandbox.io/p/sandbox/mobx-state-tree-instead-of-reducer-and-context-persistence-hjmrzg?file=%2Fsrc%2FViewModel.ts%3A70%2C59), make some changes, and then reload it. You'll see your changes have persisted. React Context can also be persisted to localStorage, but again, it requires you to write the logic from the ground up. If you need this kind of functionality in a large project, the MST community has already taken care of it for you, and we have conventions and maintainers behind the code, so you're never really on your own. ## MST is State Management on Easy Mode At this point, we hope the benefits of MobX-State-Tree are clear. If you have a complex application, or if your application is going to become complex over time, MST offers a pre-built set of tools and conventions that will allow you to focus on building features and solving user problems, rather than reinventing the wheel for your state management system. There are many more MST-specific utilities available, like [data normalization](../concepts/references.md), [JSON patches](../concepts/patches.md), [middleware](../concepts/middleware.md), and libraries like [mst-query](https://github.com/ConrabOpto/mst-query) and [mst-gql](https://github.com/mobxjs/mst-gql) to help you manage asynchronous state. Much like the prior examples in this article, using these tools will save you a lot of work building and maintaining your own bespoke solutions. If you've been working with React Reducer and Context, MobX-State-Tree will feel like easy-mode for state management. On top of that, you'll join a [welcoming, active community](https://github.com/mobxjs/mobx-state-tree/discussions) where we can help you with state modeling questions, and any learning curve you experience while getting used to MobX-State-Tree. Questions? Comments? [Let us know in the forum](https://github.com/mobxjs/mobx-state-tree/discussions) ================================================ FILE: docs/concepts/actions.md ================================================ --- id: actions title: Actions ---
egghead.io lesson 2: Attach Behavior to mobx-state-tree Models Using Actions
Hosted on egghead.io
By default, nodes can only be modified by one of their actions, or by actions higher up in the tree. Actions can be defined by returning an object from the action initializer function that was passed to `actions`. The initializer function is executed for each instance, so that `self` is always bound to the current instance. Also, the closure of that function can be used to store so called _volatile_ state for the instance or to create private functions that can only be invoked from the actions, but not from the outside. ```javascript const Todo = types .model({ title: types.string }) .actions(self => { function setTitle(newTitle) { self.title = newTitle } return { setTitle } }) ``` Shorter form if no local state or private functions are involved: ```javascript const Todo = types .model({ title: types.string }) .actions(self => ({ // note the `({`, we are returning an object literal setTitle(newTitle) { self.title = newTitle } })) ``` Actions are replayable and are therefore constrained in several ways: - Trying to modify a node without using an action will throw an exception. - It's recommended to make sure action arguments are serializable. Some arguments can be serialized automatically such as relative paths to other nodes - Actions can only modify models that belong to the (sub)tree on which they are invoked - You cannot use `this` inside actions. Instead, use `self`. This makes it safe to pass actions around without binding them or wrapping them in arrow functions. Useful methods: - [`onAction`](/API/#onaction) listens to any action that is invoked on the model or any of its descendants. - [`addMiddleware`](/API/#addmiddleware) adds an interceptor function to any action invoked on the subtree. - [`applyAction`](/API/#applyaction) invokes an action on the model according to the given action description #### Action listeners versus middleware The difference between action listeners and middleware is: middleware can intercept the action that is about to be invoked, modify arguments, return types, etc. Action listeners cannot intercept and are only notified. Action listeners receive the action arguments in a serializable format, while middleware receives the raw arguments. (`onAction` is actually just a built-in middleware). For more details on creating middleware, see the [docs](/concepts/middleware). #### Disabling protected mode This may be desired if the default protection of `mobx-state-tree` doesn't fit your use case. For example, if you are not interested in replayable actions or hate the effort of writing actions to modify any field, `unprotect(tree)` will disable the protected mode of a tree allowing anyone to directly modify the tree. ================================================ FILE: docs/concepts/async-actions.md ================================================ --- id: async-actions title: Asynchronous actions ---
egghead.io lesson 12: Defining Asynchronous Processes Using Flow
Hosted on egghead.io
The recommended way to write asynchronous actions is by using `flow` and generators. They always return a promise, and work for all practical purposes the same as async / await. For a real working example see the [bookshop sources](https://github.com/mobxjs/mobx-state-tree/blob/adba1943af263898678fe148a80d3d2b9f8dbe63/examples/bookshop/src/stores/BookStore.js#L25). A detailed break-down is made below, but a quick example to get the gist: _Warning: don't import `flow` from `"mobx"`, but from `"mobx-state-tree"` instead!_ ```javascript import { types, flow } from "mobx-state-tree" someModel.actions((self) => { const fetchProjects = flow(function* () { // <- note the star, this is a generator function! self.state = "pending" try { // ... yield can be used in async/await style self.githubProjects = yield fetchGithubProjectsSomehow() self.state = "done" } catch (error) { // ... including try/catch error handling console.error("Failed to fetch projects", error) self.state = "error" } // The action will return a promise that resolves to the returned value // (or rejects with anything thrown from the action) return self.githubProjects.length }) return { fetchProjects } }) ``` # Creating asynchronous actions Asynchronous actions are a first class concept in Mobx-State-Tree. Modelling an asynchronous flow can be done in two ways: 1. Model each step of the flow as separate action 2. Use generators The recommended approach is to use _generators_, for reasons mentioned below. But let's take a look at modelling asynchronous actions as a set of actions first. ## Using separate actions MST doesn't allow changing state outside actions (except when the tree is unprotected). This means that each step in an asynchronous flow that needs to actually change the model needs to become a separate action. For example: ```javascript const Store = types .model({ githubProjects: types.array(types.frozen), state: types.enumeration("State", ["pending", "done", "error"]) }) .actions((self) => ({ fetchProjects() { self.githubProjects = [] self.state = "pending" fetchGithubProjectsSomehow().then( // when promise resolves, invoke the appropiate action // (note that there is no need to bind here) self.fetchProjectsSuccess, self.fetchProjectsError ) }, fetchProjectsSuccess(projects) { self.state = "done" self.githubProjects = projects }, fetchProjectsError(error) { console.error("Failed to fetch projects", error) self.state = "error" } })) ``` This approach works fine and has great type inference, but comes with a few downsides: 1. For complex flows, which update data in the middle of the flow, a lot of "utility" actions need to be created. 2. Each step of the flow is exposed as action to the outside world. In the above example, one could (but shouldn't) directly invoke `store.fetchProjectsSuccess([])` 3. Middleware cannot distinguish the flow initiating action from the handler actions. This means that actions like `fetchProjectsSuccess` will become part of the recorded action list, although you probably never want to replay it (as replaying `fetchProjects` itself will cause the handler actions to be fired in the end). ## Using generators Generators might sound scary, but they are very suitable for expressing asynchronous flows. The above example looks as follows when using generators: ```javascript import { flow } from "mobx-state-tree" const Store = types .model({ githubProjects: types.array(types.frozen), state: types.enumeration("State", ["pending", "done", "error"]) }) .actions((self) => ({ fetchProjects: flow(function* fetchProjects() { // <- note the star, this a generator function! self.githubProjects = [] self.state = "pending" try { // ... yield can be used in async/await style self.githubProjects = yield fetchGithubProjectsSomehow() self.state = "done" } catch (error) { // ... including try/catch error handling console.error("Failed to fetch projects", error) self.state = "error" } }) })) const store = Store.create({}) // async actions will always return a promise resolving to the returned value store.fetchProjects().then(() => { console.log("done") }) ``` Creating asynchronous actions using generators works as follow: 1. The action needs to be marked as generator, by postfixing the `function` keyword with a `*` and a name (which will be used by middleware), and wrapping it with `flow` 2. The action can be paused by using a `yield` statement. Yield always needs to return a `Promise`. 3. If the promise resolves, the resolved value will be returned from the `yield` statement, and the action will continue to run 4. If the promise rejects, the action continues and the rejection reason will be thrown from the `yield` statement 5. Invoking the asynchronous action returns a promise. That will resolve with the return value of the function, or rejected with any exception that escapes from the asynchronous actions. > Note: `flow()` is available in `v1.1.0` and above. If you see an error message like: `_mobxStateTree.flow is not a function`, check your version and upgrade if necessary. Using generators is syntactically clean. But the main advantage is that they receive first class support from MST. Middleware (see below) can implement specific behavior for asynchronous actions. For example, the `onAction` middleware will only record starting asynchronous flows, but not any async steps that are taking during the flow. After all, when replaying the invocation will lead to the other steps being executed automatically. Besides that, each step in the generator is allowed to modify its own instance, and there is no need to expose the individual flow steps as actions. See the [bookshop example sources](https://github.com/coolsoftwaretyler/mst-example-bookshop/blob/main/src/stores/BookStore.js#L25) for a more extensive example. Using generators requires Promises and generators to be available. Promises can easily be polyfilled although they tend to be available on every modern JS environment. Generators are well supported as well, and both TypeScript and Babel can compile generators to ES5. To see how `flows`s can be monitored and detected in middleware, see the [middleware docs](middleware.md). ## What about async / await? Async/await can only be used in trees that are unprotected. Async / await is not flexible enough to allow MST to wrap asynchronous steps in actions automatically, as is done for the generator functions. Luckily, using generators in combination with `flow` is very similar to `async / await`: `async function() {}` becomes `flow(function* () {})`, and `await promise` becomes `yield promise`, and further behavior should be the same. ================================================ FILE: docs/concepts/dependency-injection.md ================================================ --- id: dependency-injection title: Dependency Injection ---
When creating a new state tree it is possible to pass in environment specific data by passing an object as the second argument to a `.create` call. This object should be (shallowly) immutable and can be accessed by any model in the tree by calling `getEnv(self)`. This is useful to inject environment or test-specific utilities like a transport layer, loggers, etc. This is also very useful to mock behavior in unit tests or provide instantiated utilities to models without requiring singleton modules. See also the [bookshop example](https://github.com/mobxjs/mobx-state-tree/blob/a4f25de0c88acf0e239acb85e690e91147a8f0f0/examples/bookshop/src/stores/ShopStore.test.js#L9) for inspiration. ```javascript import { types, getEnv } from "mobx-state-tree" const Todo = types .model({ title: "" }) .actions(self => ({ setTitle(newTitle) { // grab injected logger and log getEnv(self).logger.log("Changed title to: " + newTitle) self.title = newTitle } })) const Store = types.model({ todos: types.array(Todo) }) // setup logger and inject it when the store is created const logger = { log(msg) { console.log(msg) } } const store = Store.create( { todos: [{ title: "Grab tea" }] }, { logger: logger // inject logger to the tree } ) store.todos[0].setTitle("Grab coffee") // prints: Changed title to: Grab coffee ``` ================================================ FILE: docs/concepts/listeners.md ================================================ --- id: listeners title: Listening to observables, snapshots, patches and actions sidebar_label: Listening to changes ---
MST is powered by MobX. This means that it is immediately compatible with `observer` components or reactions like `autorun`: ```javascript import { autorun } from "mobx" autorun(() => { console.log(storeInstance.selectedTodo.title) }) ``` Because MST keeps immutable snapshots in the background, it is also possible to be notified when a new snapshot of the tree is available. This is similar to `.subscribe` on a redux store: ```javascript onSnapshot(storeInstance, (newSnapshot) => { console.info("Got new snapshot:", newSnapshot) }) ``` However, sometimes it is more useful to precisely know what has changed rather than just receiving a complete new snapshot. For that, MST supports json-patches out of the box. ```javascript onPatch(storeInstance, patch => { console.info("Got change: ", patch) }) storeInstance.todos[0].setTitle("Add milk") // prints: { path: "/todos/0", op: "replace", value: "Add milk" } ``` Similarly, you can be notified whenever an action is invoked by using `onAction`. ```javascript onAction(storeInstance, call => { console.info("Action was called:", call) }) storeInstance.todos[0].setTitle("Add milk") // prints: { path: "/todos/0", name: "setTitle", args: ["Add milk"] } ``` It is even possible to intercept actions before they are applied by adding middleware using `addMiddleware`: ```javascript addMiddleware(storeInstance, (call, next) => { call.args[0] = call.args[0].replace(/tea/gi, "Coffee") return next(call) }) ``` A more extensive middleware example can be found in this [code sandbox](https://codesandbox.io/s/0yt72). For more details on creating middleware and the exact specification of middleware events, see the [docs](middleware). Finally, it is not only possible to be notified about snapshots, patches or actions. It is also possible to re-apply them by using `applySnapshot`, `applyPatch` or `applyAction`! ================================================ FILE: docs/concepts/middleware.md ================================================ --- id: middleware title: Middleware ---
Middlewares can be used to intercept any action on a subtree. It is allowed to attach multiple middlewares to a node. The order in which middlewares are invoked is inside-out: This means that the middlewares are invoked in the order you attach them. The value returned by the action invoked/ the aborted value gets passed through the middleware chain and can be manipulated. The community has created a small set of [pre-built / example middlewares](https://github.com/coolsoftwaretyler/mst-middlewares). Play around with a simple example of middleware in action with [this CodeSandbox](https://codesandbox.io/s/vjoql07ool). ## Custom Middleware Middlewares can be attached by using: `addMiddleware(target: IAnyStateTreeNode, handler: IMiddlewareHandler, includeHooks: boolean = true) : IDisposer` ### target the middleware will only be attached to actions of the `target` and further sub nodes of such. ### handler An example of this is as follows: ```js const store = SomeStore.create() const disposer = addMiddleware(store, (call, next, abort) => { console.log(`action ${call.name} was invoked`) // runs the next middleware // or the implementation of the targeted action // if there is no middleware left to run // the value returned from the next can be manipulated next(call, (value) => value + 1) }) ``` ```js const store = SomeStore.create() const disposer = addMiddleware(store, (call, next, abort) => { console.log(`action ${call.name} was invoked`) // aborts running the middlewares and returns the 'value' instead. // note that the targeted action won't be reached either. return abort("value") }) ``` A middleware handler receives three arguments: 1. the description of the the call, - a function to invoke the next middleware in the chain and manipulate the returned value from the next middleware in the chain. - a function to abort the middleware queue and return a value. _Note: You must call either `next(call)` or `abort(value)` within a middleware._ _Note: If you abort, the action invoked will never be reached._ _Note: The value from either `abort('value')` or the returned value from the `action` can be manipulated by previous middlewares._ _Note: It is important to invoke `next(call)` or `abort(value)` synchronously._ _Note: The value of the `abort(value)` must be a promise in case of aborting a `flow`._ #### call ```javascript export type IMiddleWareEvent = { type: IMiddlewareEventType name: string id: number parentId: number rootId: number allParentIds: number[] tree: IStateTreeNode context: IStateTreeNode args: any[] } export type IMiddlewareEventType = | "action" | "flow_spawn" | "flow_resume" | "flow_resume_error" | "flow_return" | "flow_throw" ``` - `name` is the name of the action - `context` is the object on which the action was defined & invoked - `tree` is the root of the MST tree in which the action was fired (`tree === getRoot(context)`) - `args` are the original arguments passed to the action - `id` is a number that is unique per external action invocation. - `parentId` is the number of the action / process that called this action. `0` if it wasn't called by another action but directly from user code - `rootid` is the id of the action that spawned this action. If an action was not spawned by another action, but something external (user event etc), `id` and `rootId` will be equal (and `parentid` `0`) - `allParentIds` is the chain from root until current (excluding current) that called this action. `[]` if it wasn't called by another action but directly from user code `type` Indicates which kind of event this is - `action`: this is a normal synchronous action invocation - `flow_spawn`: The invocation / kickoff of a `process` block (see [asynchronous actions](async-actions.md)) - `flow_resume`: a promise that was returned from `yield` earlier has resolved. `args` contains the value it resolved to, and the action will now continue with that value - `flow_resume_error`: a promise that was returned from `yield` earlier was rejected. `args` contains the rejection reason, and the action will now continue throwing that error into the generator - `flow_return`: the generator completed successfully. The promise returned by the action will resolve with the value found in `args` - `flow_throw`: the generator threw an uncatched exception. The promise returned by the action will reject with the exception found in `args` To see how a bunch of calls from an asynchronous process look, see the [unit tests](https://github.com/mobxjs/mobx-state-tree/blob/09708ba86d04f433cc23fbcb6d1dc4db170f798e/test/async.ts#L289) A minimal, empty process will fire the following events if started as action: 1. `action`: An `action` event will always be emitted if a process is exposed as action on a model) 2. `flow_spawn`: This is just the notification that a new generator was started 3. `flow_resume`: This will be emitted when the first "code block" is entered. (So, with zero yields there is one `flow_resume` still) 4. `flow_return`: The process has completed #### next use next to call the next middleware. `next(call: IMiddlewareEvent, callback?: (value: any) => any): void` - `call` Before passing the call middleware, feel free to (clone and) modify the `call.args`. Other properties should not be modified - `callback` can be used to manipulate values returned by later middlewares or the implementation of the targeted action. #### abort use `abort` if you want to kill the queue of middlewares and immediately return. the implementation of the targeted action won't be reached if you abort the queue. `abort(value: any) : void` - `value` is returned instead of the return value from the implementation of the targeted action. ### includeHooks set this flag to `false` if you want to avoid having hooks passed to the middleware. ## FAQ - I alter a property and the change does not appear in the middleware. - _If you alter a value of an unprotected node, the change won't reach the middleware. Only actions can be intercepted._ ================================================ FILE: docs/concepts/patches.md ================================================ --- id: patches title: Patches ---
egghead.io lesson 3: Test mobx-state-tree Models by Recording Snapshots or Patches
Hosted on egghead.io
Modifying a model does not only result in a new snapshot, but also in a stream of [JSON-patches](http://jsonpatch.com/) describing which modifications were made. Patches have the following signature: export interface IJsonPatch { op: "replace" | "add" | "remove" path: string value?: any } - Patches are constructed according to JSON-Patch, RFC 6902 - Patches are emitted immediately when a mutation is made and don't respect transaction boundaries (like snapshots) - Patch listeners can be used to achieve deep observing - The `path` attribute of a patch contains the path of the event relative to the place where the event listener is attached - A single mutation can result in multiple patches, for example when splicing an array - Patches can be reverse applied, which enables many powerful patterns like undo / redo Useful methods: - `onPatch(model, listener)` attaches a patch listener to the provided model, which will be invoked whenever the model or any of its descendants is mutated - `applyPatch(model, patch)` applies a patch (or array of patches) to the provided model ================================================ FILE: docs/concepts/react.md ================================================ --- id: using-react title: React and MST ---
egghead.io lesson 5: Render mobx-state-tree Models in React
Hosted on egghead.io
### Can I use React and MST together? Yep, that works perfectly fine, everything that applies to MobX and React applies to MST and React as well. `observer`, `autorun`, etc. will work as expected. To share MST trees between components we recommend to use `React.createContext`. In the examples folder several examples of React and MST can be found, or check this [example](https://github.com/impulse/react-hooks-mobx-state-tree) which uses hooks (recommended). ### Tips When passing models in to a component **do not** use the spread syntax, e.g. ``. See [here](https://github.com/mobxjs/mobx-state-tree/issues/726). ================================================ FILE: docs/concepts/reconciliation.md ================================================ --- id: reconciliation title: Reconciliation ---
- When applying snapshots, MST will always try to reuse existing object instances for snapshots with the same identifier (see `types.identifier`). - If no identifier is specified, but the type of the snapshot is correct, MST will reconcile objects as well if they are stored in a specific model property or under the same map key. - In arrays, items without an identifier are never reconciled. If an object is reconciled, the consequence is that localState is preserved and `afterCreate` / `attach` life-cycle hooks are not fired because applying a snapshot results just in an existing tree node being updated. ================================================ FILE: docs/concepts/references.md ================================================ --- id: references title: Identifiers and references ---
egghead.io lesson 13: Create Relationships in your Data with mobx-state-tree Using References and Identifiers
Hosted on egghead.io
References and identifiers are a first-class concept in MST. This makes it possible to declare references and keep the data normalized in the background, while you interact with it in a denormalized manner. Example: ```javascript const Todo = types.model({ id: types.identifier, title: types.string }) const TodoStore = types.model({ todos: types.array(Todo), selectedTodo: types.reference(Todo) }) // create a store with a normalized snapshot const storeInstance = TodoStore.create({ todos: [ { id: "47", title: "Get coffee" } ], selectedTodo: "47" }) // because `selectedTodo` is declared to be a reference, it returns the actual Todo node with the matching identifier console.log(storeInstance.selectedTodo.title) // prints "Get coffee" ``` #### Identifiers - Each model can define zero or one `identifier()` properties - The identifier property of an object cannot be modified after initialization - Each identifier / type combination should be unique within the entire tree - Identifiers are used to reconcile items inside arrays and maps - wherever possible - when applying snapshots - The `map.put()` method can be used to simplify adding an object that has an identifiers to a map without specifying the key - The primary goal of identifiers is not validation, but reconciliation and reference resolving. For this reason identifiers cannot be defined or updated after creation. If you want to check if some value just looks as an identifier, without providing the above semantics; use something like: `types.refinement(types.string, v => v.match(/someregex/))` _Tip: If you know the format of the identifiers in your application, leverage `types.refinement` to actively check this, for example the following definition enforces that identifiers of `Car` always start with the string `"Car_"`:_ ```javascript const Car = types.model("Car", { id: types.refinement(types.identifier, identifier => identifier.indexOf("Car_") === 0) }) ``` #### References References are defined by mentioning the type they should resolve to. The targeted type should have exactly one attribute of the type `identifier`. References are looked up through the entire tree but per type, so identifiers need to be unique in the entire tree. #### Customizable references The default implementation uses the `identifier` cache to resolve references (See [`resolveIdentifier`](/API#resolveidentifier)). However, it is also possible to override the resolve logic and provide your own custom resolve logic. This also makes it possible to, for example, trigger a data fetch when trying to resolve the reference ([example](https://github.com/mobxjs/mobx-state-tree/blob/master/__tests__/core/reference-custom.test.ts#L127)). Example: ```javascript const User = types.model({ id: types.identifier, name: types.string }) const UserByNameReference = types.maybeNull( types.reference(User, { // given an identifier, find the user get(identifier /* string */, parent: any /*Store*/) { return parent.users.find(u => u.name === identifier) || null }, // given a user, produce the identifier that should be stored set(value /* User */) { return value.name } }) ) const Store = types.model({ users: types.array(User), selection: UserByNameReference }) const s = Store.create({ users: [{ id: "1", name: "Michel" }, { id: "2", name: "Mattia" }], selection: "Mattia" }) ``` #### Reference validation: `isValidReference`, `tryReference`, `onInvalidated` hook and `types.safeReference` Accessing an invalid reference (a reference to a dead/detached node) triggers an exception. In order to check if a reference is valid, MST offers the `isValidReference(() => ref): boolean` function: ```ts const isValid = isValidReference(() => store.myRef) ``` Also, if you are unsure if a reference is valid or not you can use the `tryReference(() => ref): ref | undefined` function: ```ts // the result will be the passed ref if ok, or undefined if invalid const maybeValidRef = tryReference(() => store.myRef) ``` The options parameter for references also accepts an optional `onInvalidated` hook, which will be called when the reference target node that the reference is pointing to is about to be detached/destroyed. It has the following signature: ```ts const refWithOnInvalidated = types.reference(Todo, { onInvalidated(event: { // what is causing the target to become invalidated cause: "detach" | "destroy" | "invalidSnapshotReference" // the target that is about to become invalidated (undefined if "invalidSnapshotReference") invalidTarget: STN | undefined // the identifier that is about to become invalidated invalidId: string | number // parent node of the reference (not the reference target) parent: IAnyStateTreeNode // a function to remove the reference from its parent (or set to undefined in the case of models) removeRef: () => void // a function to set our reference to a new target replaceRef: (newRef: STN | null | undefined) => void }) { // do something } }) ``` Note that invalidation will only trigger while the reference is attached to a parent (be it a model, an array, a map, etc.). A default implementation of such `onInvalidated` hook is provided by the `types.safeReference` type. It is like a standard reference, except that once the target node becomes invalidated it will: - If its parent is a model: Set its own property to `undefined` - If its parent is an array: Remove itself from the array - If its parent is a map: Remove itself from the map In addition to the options possible for a plain reference type, the optional options parameter object also accepts a parameter named `acceptsUndefined`, which is set to true by default, so it is suitable for model properties. When used inside collections (arrays/maps) it is recommended to set this option to false so it can't take undefined as value, which is usually the desired in those cases. Strictly speaking, `safeReference` with `acceptsUndefined` set to true (the default) is implemented as ```js types.maybe( types.reference(Type, { ...customGetSetIfAvailable, onInvalidated(ev) { ev.removeRef() } }) ) ``` and with `acceptsUndefined` set to false as ```js types.reference(Type, { ...customGetSetIfAvailable, onInvalidated(ev) { ev.removeRef() } }) ``` ```js const Todo = types.model({ id: types.identifier }) const Store = types.model({ todos: types.array(Todo), selectedTodo: types.safeReference(Todo), multipleSelectedTodos: types.array(types.safeReference(Todo, { acceptsUndefined: false })) }) // given selectedTodo points to a valid Todo and that Todo is later removed from the todos // array, then selectedTodo will automatically become undefined, and if it is included in multipleSelectedTodos // then it will be removed from the array ``` ================================================ FILE: docs/concepts/snapshots.md ================================================ --- id: snapshots title: Snapshots ---
egghead.io lesson 3: Test mobx-state-tree Models by Recording Snapshots or Patches
Hosted on egghead.io
egghead.io lesson 9: Store Store in Local Storage
Hosted on egghead.io
egghead.io lesson 16: Automatically Send Changes to the Server by Using onSnapshot
Hosted on egghead.io
Snapshots are the immutable serialization, in plain objects, of a tree at a specific point in time. Snapshots can be inspected through `getSnapshot(node, applyPostProcess)`. Snapshots don't contain any type information and are stripped from all actions, etc., so they are perfectly suitable for transportation. Requesting a snapshot is cheap as MST always maintains a snapshot of each node in the background and uses structural sharing. ```javascript coffeeTodo.setTitle("Tea instead plz") console.dir(getSnapshot(coffeeTodo)) // prints `{ title: "Tea instead plz" }` ``` Some interesting properties of snapshots: - Snapshots are immutable - Snapshots can be transported - Snapshots can be used to update models or restore them to a particular state - Snapshots are automatically converted to models when needed. So, the two following statements are equivalent: `store.todos.push(Todo.create({ title: "test" }))` and `store.todos.push({ title: "test" })`. Useful methods: - `getSnapshot(model, applyPostProcess)`: returns a snapshot representing the current state of the model - `onSnapshot(model, callback)`: creates a listener that fires whenever a new snapshot is available (but only one per MobX transaction). - `applySnapshot(model, snapshot)`: updates the state of the model and all its descendants to the state represented by the snapshot `mobx-state-tree` also supports customizing snapshots when they are generated or when they are applied with [`types.snapshotProcessor`](/overview/hooks). ================================================ FILE: docs/concepts/trees.md ================================================ --- id: trees title: Types, models, trees & state ---
### tree = type + state Each **node** in the tree is described by two things: Its **type** (the shape of the thing) and its **data** (the state it is currently in). The simplest tree possible: ```javascript import { types } from "mobx-state-tree" // alternatively, `import { t } from "mobx-state-tree"` // declaring the shape of a node with the type `Todo` const Todo = types.model({ title: types.string }) // creating a tree based on the "Todo" type, with initial data: const coffeeTodo = Todo.create({ title: "Get coffee" }) ``` The `types.model` type declaration is used to describe the shape of an object. Other built-in types include arrays, maps, primitives, etc. See the [types overview](/overview/types). ### Creating models
egghead.io lesson 1: Describe Your Application Domain Using mobx-state-tree(MST) Models
Hosted on egghead.io
The most important type in MST is `types.model`, which can be used to describe the shape of an object. An example: ```javascript const TodoStore = types // 1 .model("TodoStore", { loaded: types.boolean, // 2 endpoint: "http://localhost", // 3 todos: types.array(Todo), // 4 selectedTodo: types.reference(Todo) // 5 }) .views((self) => { return { // 6 get completedTodos() { return self.todos.filter((t) => t.done) }, // 7 findTodosByUser(user) { return self.todos.filter((t) => t.assignee === user) } } }) .actions((self) => { return { addTodo(title) { self.todos.push({ id: Math.random(), title }) } } }) ``` When defining a model, it is advised to give the model a name for debugging purposes (see `// 1`). A model takes additionally object argument defining the properties. The _properties_ argument is a key-value set where each key indicates the introduction of a property, and the value its type. The following types are acceptable: 1. A type. This can be a simple primitive type like `types.boolean`, see `// 2`, or a complex, possibly pre-defined type (`// 4`) 2. A primitive. Using a primitive as type is syntactic sugar for introducing a property with a default value. See `// 3`, `endpoint: "http://localhost"` is the same as `endpoint: types.optional(types.string, "http://localhost")`. The primitive type is inferred from the default value. Properties with a default value can be omitted in snapshots. 3. A [computed property](https://mobx.js.org/computeds.html), see `// 6`. Computed properties are tracked and memoized by MobX. Computed properties will not be stored in snapshots or emit patch events. It is possible to provide a setter for a computed property as well. A setter should always invoke an action. 4. A view function (see `// 7`). A view function can, unlike computed properties, take arbitrary arguments. It won't be memoized, but its value can be tracked by MobX nonetheless. View functions are not allowed to change the model, but should rather be used to retrieve information from the model. _Tip: `(self) => ({ action1() { }, action2() { }})` is ES6 syntax for `function (self) { return { action1: function() { }, action2: function() { } }}`. In other words, it's short way of directly returning an object literal. For that reason a comma between each member of a model is mandatory, unlike classes which are syntactically a totally different concept._ `types.model` creates a chainable model type, where each chained method produces a new type: - `.named(name)` clones the current type, but gives it a new name - `.props(props)` produces a new type, based on the current one, and adds / overrides the specified properties - `.actions(self => object literal with actions)` produces a new type, based on the current one, and adds / overrides the specified actions - `.views(self => object literal with view functions)` produces a new type, based on the current one, and adds / overrides the specified view functions - `.preProcessSnapshot(snapshot => snapshot)` can be used to pre-process the raw JSON before instantiating a new model. See [Lifecycle hooks](/overview/hooks) or alternatively `types.snapshotProcessor` - `.postProcessSnapshot(snapshot => snapshot)` can be used to post-process the raw JSON before getting a model snapshot. See [Lifecycle hooks](/overview/hooks) or alternatively `types.snapshotProcessor` Note that `views` and `actions` don't define actions and views directly, but rather they should be given a function. The function will be invoked when a new model instance is created. The instance will be passed in as the first and only argument typically called `self`. This has two advantages: 1. All methods will always be bound correctly, and won't suffer from an unbound `this` 2. The closure can be used to store private state or methods of the instance. See also [actions](/concepts/actions) and [volatile state](/concepts/volatiles). Quick example: ```javascript const TodoStore = types .model("TodoStore", { /* props */ }) .actions((self) => { const instantiationTime = Date.now() function addTodo(title) { console.log(`Adding Todo ${title} after ${(Date.now() - instantiationTime) / 1000}s.`) self.todos.push({ id: Math.random(), title }) } return { addTodo } }) ``` It is perfectly fine to chain multiple `views`, `props` calls etc in arbitrary order. This can be a great way to structure complex types, mix-in utility functions, etc. Each call in the chain creates a new, immutable type which can itself be stored and reused as part of other types, etc. It is also possible to define lifecycle hooks in the _actions_ object. These are actions with a predefined name that are run at a specific moment. See [Lifecycle hooks](/overview/hooks). ### Composing trees In MST every node in the tree is a tree in itself. Trees can be composed by composing their types: ```javascript const TodoStore = types.model({ todos: types.array(Todo) }) const storeInstance = TodoStore.create({ todos: [ { title: "Get biscuit" } ] }) ``` The _snapshot_ passed to the `create` method of a type will recursively be turned in MST nodes. So, you can safely call: ```javascript storeInstance.todos[0].setTitle("Chocolate instead plz") ``` Because any node in a tree is a tree in itself, any built-in method in MST can be invoked on any node in the tree, not just the root. This makes it possible to get a patch stream of a certain subtree, or to apply middleware to a certain subtree only. ### Tree semantics in detail MST trees have very specific semantics. These semantics purposefully constrain what you can do with MST. The reward for that is all kinds of generic features out of the box like snapshots, replayability, etc. If these constraints don't suit your app, you are probably better off using plain MobX with your own model classes, which is fine as well. 1. Each object in an MST tree is considered a _node_. Each primitive (and frozen) value is considered a _leaf_. 1. MST has only three types of nodes: _model_, _array_ and _map_. 1. Every _node_ tree in an MST tree is a tree in itself. Any operation that can be invoked on the complete tree can also be applied to a subtree. 1. A node can only exist exactly _once_ in a tree. This ensures it has a unique, identifiable position. 1. It is however possible to refer to another object in the _same_ tree by using _references_ 1. There is no limit to the number of MST trees that live in an application. However, each node can only live in exactly one tree. 1. All _leaves_ in the tree must be serializable. It is not possible to store, for example, functions in a MST. 1. The only free-form type in MST is frozen, with the requirement that frozen values are immutable and serializable so that the MST semantics can still be upheld. 1. At any point in the tree it is possible to assign a snapshot to the tree instead of a concrete instance of the expected type. In that case an instance of the correct type, based on the snapshot, will be automatically created for you. 1. Nodes in the MST tree will be reconciled (the exact same instance will be reused) when updating the tree by any means, based on their _identifier_ property. If there is no identifier property, instances won't be reconciled. 1. If a node in the tree is replaced by another node, the original node will die and become unusable. This makes sure you are not accidentally holding on to stale objects anywhere in your application. 1. If you want to create a new node based on an existing node in a tree, you can either `detach` that node, or `clone` it. These egghead.io lessons nicely leverage the specific semantics of MST trees:
egghead.io lesson 6: Build Forms with React to Edit mobx-state-tree Models
Hosted on egghead.io
egghead.io lesson 7: Remove Model Instances from the Tree
Hosted on egghead.io
egghead.io lesson 8: Create an Entry Form to Add Models to the State Tree
Hosted on egghead.io
================================================ FILE: docs/concepts/views.md ================================================ --- id: views title: Derived values ---
egghead.io lesson 4: Derive Information from Models Using Views
Hosted on egghead.io
Any fact that can be derived from your state is called a "view" or "derivation". See [The gist of MobX](https://mobx.js.org/the-gist-of-mobx.html) for some background. Views come in two flavors: views with arguments and views without arguments. The latter are called computed values, based on the [computed](https://mobx.js.org/computeds.html) concept in MobX. The main difference between the two is that computed properties create an explicit caching point, but later they work the same and any other computed value or MobX based reaction like [`@observer`](https://mobx.js.org/react-integration.html) components can react to them. Computed values are defined using _getter_ functions. Example: ```javascript import { autorun } from "mobx" const UserStore = types .model({ users: types.array(User) }) .views(self => ({ get numberOfChildren() { return self.users.filter(user => user.age < 18).length }, numberOfPeopleOlderThan(age) { return self.users.filter(user => user.age > age).length } })) const userStore = UserStore.create(/* */) // Every time the userStore is updated in a relevant way, log messages will be printed autorun(() => { console.log("There are now ", userStore.numberOfChildren, " children") }) autorun(() => { console.log("There are now ", userStore.numberOfPeopleOlderThan(75), " pretty old people") }) ``` If you want to share volatile state between views and actions, use `.extend` instead of `.views` + `.actions`. See the [volatile state](volatiles) section. ================================================ FILE: docs/concepts/volatiles.md ================================================ --- id: volatiles title: Volatile state ---
egghead.io lesson 15: Use Volatile State and Lifecycle Methods to Manage Private State
Hosted on egghead.io
MST models primarily aid in storing _persistable_ state. State that can be persisted, serialized, transferred, patched, replaced, etc. However, sometimes you need to keep track of temporary, non-persistable state. This is called _volatile_ state in MST. Examples include promises, sockets, DOM elements, etc. - state which is needed for local purposes as long as the object is alive. Volatile state (which is also private) can be introduced by creating variables inside any of the action initializer functions. Volatile is preserved for the life-time of an object and not reset when snapshots are applied, etc. Note that the life time of an object depends on proper reconciliation, see the [how does reconciliation work?](reconciliation) section. The following is an example of an object with volatile state. Note that volatile state here is used to track a XHR request and clean up resources when it is disposed. Without volatile state this kind of information would need to be stored in an external WeakMap or something similar. ```javascript const Store = types .model({ todos: types.array(Todo), state: types.enumeration("State", ["loading", "loaded", "error"]) }) .actions(self => { let pendingRequest = null // a Promise function afterCreate() { self.state = "loading" pendingRequest = someXhrLib.createRequest("someEndpoint") } function beforeDestroy() { // abort the request, no longer interested pendingRequest.abort() } return { afterCreate, beforeDestroy } }) ``` Some tips: 1. Note that multiple `actions` calls can be chained. This makes it possible to create multiple closures with their own protected volatile state. 2. Although in the above example the `pendingRequest` could be initialized directly in the action initializer, it is recommended to do this in the `afterCreate` hook, which will only be called once the entire instance has been set up (there might be many action and property initializers for a single type). 3. The above example doesn't actually use the promise. For how to work with promises / asynchronous flows, see the [asynchronous actions](async-actions) section. 4. It is possible to share volatile state between views and actions by using `extend`. `.extend` works like a combination of `.actions` and `.views` and should return an object with a `actions` and `views` field: Here's an example of how to do your own volatile state using an observable: ```javascript // if your local state is part of a view getter (computed) then // it is important to make sure that state used such getters are observable, // or else the value returned by the view would become stale upon observation const Todo = types.model({}).extend(self => { const localState = observable.box(3) return { views: { // note this one IS a getter (computed value) get x() { return localState.get() } }, actions: { setX(value) { localState.set(value) } } } }) ``` And here's an example of how to do your own volatile state _not_ using an observable (but if you do this make sure the local state will _never_ be used in a computed value first and bear in mind it _won't_ be reactive!): ```javascript // if not using an observable then make sure your local state is NOT part of a view getter or computed value of any kind! // also changes to it WON'T be reactive const Todo = types.model({}).extend(self => { let localState = 3 return { views: { // note this one is NOT a getter (NOT a computed value) // if this were a getter this value would get stale upon observation getX() { return localState } }, actions: { setX(value) { localState = value } } } }) ``` ### model.volatile Since the pattern above (having a volatile state that is _observable_ (in terms of Mobx observables) and _readable_ from outside the instance) is such a common pattern there is a shorthand to declare such properties. The example above can be rewritten as: ```javascript const Todo = types .model({}) .volatile(self => ({ localState: 3 })) .actions(self => ({ setX(value) { self.localState = value } })) ``` The object that is returned from the `volatile` initializer function can contain any piece of data and will result in an instance property with the same name. Volatile properties have the following characteristics: 1. They can be read from outside the model (if you want hidden volatile state, keep the state in your closure as shown in the previous section, and _only_ if it is not used on a view consider not making it observable) 2. The volatile properties will be only observable, see [observable _references_](https://mobx.js.org/api.html#observableref). Values assigned to them will be unmodified and not automatically converted to deep observable structures. 3. Like normal properties, they can only be modified through actions 4. Volatile props will not show up in snapshots, and cannot be updated by applying snapshots 5. Volatile props are preserved during the lifecycle of an instance. See also [reconciliation](reconciliation) 6. Changes in volatile props won't show up in the patch or snapshot stream 7. It is currently not supported to define getters / setters in the object returned by `volatile` 8. Volatile prop values aren't limited to values of MST's types and can be assigned any value. This includes [JS primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive) such as `string`, `number`, `Symbol`, [Object types](https://developer.mozilla.org/en-US/docs/Glossary/Object) such as `Function`, POJOs, classes - and platform API's like `localStorage`, `window.fetch` and basically anything you want. ================================================ FILE: docs/intro/examples.md ================================================ --- id: examples title: Examples ---
- [Bookshop](https://github.com/coolsoftwaretyler/mst-example-bookshop/) Example webshop application with references, identifiers, routing, testing, etc. - [Boxes](https://github.com/coolsoftwaretyler/mst-example-boxes) Example app where one can draw, drag, and drop boxes. With time-travelling and multi-client synchronization over websockets. - [TodoMVC](https://github.com/coolsoftwaretyler/mst-example-todomvc) Classic example app using React and MST. - [Redux TodoMVC](https://github.com/coolsoftwaretyler/mst-example-redux-todomvc) Redux TodoMVC application, except that the reducers are replaced with a MST. Tip: open the Redux devtools; they will work! ================================================ FILE: docs/intro/getting-started.md ================================================ --- id: getting-started title: Getting Started Tutorial ---
This tutorial will introduce you to the basics of `mobx-state-tree` (MST) by building a TODO application. The application will also have the ability to assign each TODO to a user. ## Prerequisites This tutorial assumes that you know the basics of how to use React. If you don't know what React is and how to use it, you may wish to read [this tutorial](https://facebook.github.io/react/tutorial/tutorial.html) first. ### Do I need to learn MobX? MST is heavily based on MobX. A basic understanding of the MobX library will help when dealing with complex situations and connecting of the data with React components. If you don't have MobX experience, don't worry, working with MST does not require any MobX API knowledge. ## How to follow this tutorial You can write the code for this tutorial in the browser using the CodeSandbox playground, or in your preferred code editor (e.g. VSCode). ### Writing code in the browser For each example you'll find a CodeSandbox playground link. You can start from the playground of each point and manually progress to the next tutorial step by using it. If you're stuck, feel free to have a sneak peak from the next playground link! :) ### Writing code in the editor Setting up the whole environment for a React project involves transpilers, bundlers, linters, etc., and setting them up may become very tedious and not fun. Thanks to `create-react-app` setting up all those tools becomes as easy as typing a couple of lines in your terminal. ``` npx create-react-app mst-todo ``` Next install `mobx`, `mobx-react-lite` and `mobx-state-tree` dependencies. ``` yarn add mobx mobx-react-lite mobx-state-tree ``` Now you can run `npm run start` and a basic React page will show up. You're all set up and can begin editing the project files! ## Overview MST is a state container that combines the simplicity and ease of mutable data with the traceability of immutable data and the reactiveness and performance of observable data. If the above sentence confused you, don't worry. We will dive deeper together and explore what it means step by step. ## Getting Started When building applications with MST, the first exercise that will help you building your application is thinking about what is the minimal set of entities and their relative attributes. In our example application we will deal with TODOs, so we need a `Todo` entity. The `Todo` entity will have a `name` and a `done` attribute to store if the `Todo` is done or not. We will also have knowledge of users, so we need a `User` entity that will have a `name` attribute and will be assignable to TODOs. So far our entities and their attributes look like this: `Todo` - name - done `User` - name ## Creating our first model Central to MST is the concept of a living tree. The tree consists of mutable, but strictly protected objects enriched with run-time type information. In other words; each tree has a shape (type information) and state (data). From this living tree, immutable and structurally shared snapshots are generated automatically. This means that in order to make our application work, we need to describe to MST how our attributes are shaped. Knowing that, MST will be able to automatically generate all those boundaries, and help us avoid making silly mistakes, like putting a string in price field or a boolean where an array is expected. The simplest way to define a model for an entity in MST is to provide sample data that will be used as defaults for it, and pass it to the `types.model` function. ```javascript import { types } from "mobx-state-tree" // alternatively, `import { t } from "mobx-state-tree"` const Todo = types.model({ name: "", done: false }) const User = types.model({ name: "" }) ``` [View sample in the playground](https://codesandbox.io/s/235jykjp90) The above code will create two models, a `Todo` and a `User` model, but as we said before, a tree model in MST consists of type information (and we just saw how to define them) and state (the instance data). So how do we create instances of the `Todo` and `User` models? ## Creating model instances (tree nodes) This can be easily done by calling `.create()` on the `Todo` and `User` models we just defined. ```javascript import { types, getSnapshot } from "mobx-state-tree" const Todo = types.model({ name: "", done: false }) const User = types.model({ name: "" }) const john = User.create() const eat = Todo.create() console.log("John:", getSnapshot(john)) console.log("Eat TODO:", getSnapshot(eat)) ``` [View sample in the playground](https://codesandbox.io/s/kkl8kn4pq5) As you will see, using models ensures that all the attributes defined will always be present and defaulted to the predefined values. If you want to change those values when creating the model instance, you can simply pass an object with the values to use into the `.create` function. ```javascript const eat = Todo.create({ name: "eat" }) console.log("Eat TODO:", getSnapshot(eat)) // => will print {name: "eat", done: false} ``` [View sample in the playground](https://codesandbox.io/s/jpmpyj7pm3) ## Meeting types When playing with this feature and passing in values to the `.create` function, you may encounter an error like this: ```javascript const eat = Todo.create({ name: "eat", done: 1 }) ``` ``` Error: [mobx-state-tree] Error while converting `{"name":"eat","done":1}` to `AnonymousModel`: at path "/done" value `1` is not assignable to type: `boolean`. ``` What does this mean? As I said before, MST nodes are type-enriched. This means that providing a value (number) of the wrong type (expected boolean) will make MST throw an error. This is very helpful when building applications, as it will keep your state consistent and avoid entering illegal states due to data of the wrong type. To be honest with you, I lied when I told you how to define models. The syntax you used was only a shortcut for the following syntax: ```javascript const Todo = types.model({ name: types.optional(types.string, ""), done: types.optional(types.boolean, false) }) const User = types.model({ name: types.optional(types.string, "") }) ``` [View sample in the playground](https://codesandbox.io/s/kx9x4973z3) The `types` namespace provided in the MST package provides a lot of useful types and utility types like array, map, maybe, refinements and unions. If you are interested in them, feel free to check out the [types overview](/overview/types) for the whole list and their parameters. In version `5.4.z` and above, MST also provides a namespace called `t`, which is the exact same export as `types`, but may offer some clarity when you are discussing things like TypeScript types and MobX-State-Tree "types" in the same context. We hope this gives user a tool to disambiguate their thoughts, both in code and person-to-person communication. We can now use this knowledge to combine models and define the root model of our store that will hold `Todo` and `User` instances in the `todos` and `users` maps. ```javascript import { types } from "mobx-state-tree" // alternatively, `import { t } from "mobx-state-tree"` const Todo = types.model({ name: types.optional(types.string, ""), done: types.optional(types.boolean, false) }) const User = types.model({ name: types.optional(types.string, "") }) const RootStore = types.model({ users: types.map(User), todos: types.optional(types.map(Todo), {}) }) const store = RootStore.create({ users: {} // users is not required really since arrays and maps are optional by default since MST3 }) ``` [View sample in the playground](https://codesandbox.io/s/kk63vox225) Notice that the `types.optional` second argument is required as long you don't pass a value in the `.create` function of the model. If you want, for example, to make the `name` or `todos` attribute required when calling `.create`, remove the `types.optional` function call and pass the `types.*` included inside. ## Modifying data MST tree nodes (model instances) can be modified using actions. Actions are collocated with your model and can easily be defined by declaring `.actions` over your model and passing it a function that accepts the model instance and returns an object with the functions that modify that tree node. For example, the following actions will be defined on the `Todo` model, and will allow you to toggle the `done` and set the `name` attribute of the provided `Todo` instance. ```javascript const Todo = types .model({ name: types.optional(types.string, ""), done: types.optional(types.boolean, false) }) .actions((self) => ({ setName(newName) { self.name = newName }, toggle() { self.done = !self.done } })) const User = types.model({ name: types.optional(types.string, "") }) const RootStore = types .model({ users: types.map(User), todos: types.map(Todo) }) .actions((self) => ({ addTodo(id, name) { self.todos.set(id, Todo.create({ name })) } })) ``` [View sample in the playground](https://codesandbox.io/s/3xw9x060mp) Please notice the use of `self`. `self` is the object being constructed when an instance of your model is created. Thanks to the `self` object, instance actions are "this-free", allowing you to be sure that they are correctly bound. Calling the actions is as simple as what you would do with plain JavaScript classes, you simply call them on a model instance! ```javascript store.addTodo(1, "Eat a cake") store.todos.get(1).toggle() ``` [View sample in the playground](https://codesandbox.io/s/r673zxw4p) ## Snapshots are awesome! Dealing with mutable data and objects makes it easy to change data on the fly, but on the other hand it makes testing hard. Immutable data makes that very easy. Is there a way to have the best of both worlds? Nature is a great example of that. Beings are living and mutable, but we may eternalize nature's beauty by taking awesome snapshots. Can we do the same with the state of our application? Thanks to MST's knowledge of models and relative property types, MST is able to generate serializable snapshots of our store! You can easily get a snapshot of the store by using the `getSnapshot` function exported by the MST package. ```javascript console.log(getSnapshot(store)) /* { "users": {}, "todos": { "1": { "name": "Eat a cake", "done": true } } } */ ``` Because the nature of state is mutable, a snapshot will be emitted whenever the state is mutated. To listen to the new snapshots, you can use `onSnapshot(store, snapshot => console.log(snapshot))` and log them as they are emitted. ## From snapshot to model As we just saw, getting a snapshot from a model instance is pretty easy, but wouldn't it be neat to be able to restore a model from a snapshot? The good news is that you can! That basically means that you can restore your objects with your custom methods by just knowing the type of the tree and its snapshot! You can perform this operation in two ways. 1. By creating a new model instance, and passing in the snapshot as argument to the `.create` function. This means that you will need to update all your store references, if used in React components, to the new one. 2. Avoiding this reference problem by applying the snapshot to an existing model instance. Properties will be updated, but the store reference will remain the same. This will trigger an operation called "reconciliation". We will talk about this phase later. ```javascript // 1st const store = RootStore.create({ users: {}, todos: { 1: { name: "Eat a cake", done: true } } }) // 2nd applySnapshot(store, { users: {}, todos: { 1: { name: "Eat a cake", done: true } } }) ``` [View sample in the playground](https://codesandbox.io/s/3x3v5kl5mq) ## Time travel The ability of getting snapshots and applying them makes implementing time travel really easy in user-land. What you need to do is listen for snapshots, store them and re-apply them to enable time travel! A sample implementation would look like this: ```javascript import { applySnapshot, onSnapshot } from "mobx-state-tree" var states = [] var currentFrame = -1 onSnapshot(store, (snapshot) => { if (currentFrame === states.length - 1) { currentFrame++ states.push(snapshot) } }) export function previousState() { if (currentFrame === 0) return currentFrame-- applySnapshot(store, states[currentFrame]) } export function nextState() { if (currentFrame === states.length - 1) return currentFrame++ applySnapshot(store, states[currentFrame]) } ``` ## Getting to the UI MST loves MobX, and is fully compatible with it's `autorun`, `reaction`, `observe` and other parts of the API. You can use the `mobx-react-lite` package to connect a MST store to a React component. More details can be found in the `mobx-react-lite` package documentation, but keep in mind that any view engine could be easily integrated with MST, just listen to `onSnapshot` and update accordingly! ```javascript import { observer } from "mobx-react-lite" const App = observer((props) => (
{Array.from(props.store.todos.values()).map((todo) => (
todo.toggle()} /> todo.setName(e.target.value)} />
))}
)) ``` [View sample in the playground](https://codesandbox.io/s/310ol795x6) ## Improving render performance If you have the React DevTools installed, enable the "Highlight Updates" check and you will see that the entire application will re-render whenever a `Todo` is toggled or a `name` is changed. That's a shame, as this can cause performance issues if there's a lot of `Todo`'s in our list! Thanks to the ability of MobX to emit granular updates, fixing that becomes pretty easy! You just need to split the rendering of a `Todo` into another component to only re-render that component whenever the `Todo` data changes. ```javascript const TodoView = observer((props) => (
props.todo.toggle()} /> props.todo.setName(e.target.value)} />
)) const AppView = observer((props) => (
{Array.from(props.store.todos.values()).map((todo) => ( ))}
)) ``` [View sample in the playground](https://codesandbox.io/s/jvmw9oxyxv) Each `observer` declaration will enable the React component to only re-render if any of it's observed data changes. Since our `App` component was observing everything, it was re-rendering whenever you changed something. Now that we have split the rendering logic out into a separate observer, the `TodoView` will re-render only if that `Todo` changes, and `AppView` will re-render only if a new `Todo` is added or removed since it's observing only the length of the `todos` map. ## Computed properties We now want to display the count of TODOs to be done in our application, to help users know how many TODOs are left. That means that we need to count the number of TODOs with `done` set to `false`. To do this, we need to modify the `RootStore` declaration and add a getter property over our model by calling `.views` that will count how many TODOs are left. ```javascript const RootStore = types .model({ users: types.map(User), todos: types.map(Todo) }) .views((self) => ({ get pendingCount() { return Array.from(self.todos.values()).filter((todo) => !todo.done).length }, get completedCount() { return Array.from(self.todos.values()).filter((todo) => todo.done).length } })) .actions((self) => ({ addTodo(id, name) { self.todos.set(id, Todo.create({ name })) } })) ``` [View sample in the playground](https://codesandbox.io/s/7z01y57no0) These properties are called "computed" because they keep track of the changes to the observed attributes and recompute automatically if anything used by that attribute changes. This allows for performance savings; for example changing the `name` of a TODO won't affect the number of pending and completed count, as such it wont trigger a recalculation of those counters. We can easily see that by creating an additional component in our application that observes the store and renders those counters. Using the React DevTools and tracing updates, you'll see that changing the `name` of a TODO won't re-render the counters, while checking completed or uncompleted will re-render the `TodoView` and `TodoCounterView`. ```javascript const TodoCounterView = observer((props) => (
{props.store.pendingCount} pending, {props.store.completedCount} completed
)) const AppView = observer((props) => (
{Array.from(props.store.todos.values()).map((todo) => ( ))}
)) ``` [View sample in the playground](https://codesandbox.io/s/k21ol780xr) If you `console.log` your snapshot you'll notice that computed properties won't appear in snapshots. That's fine and intended, since those properties must be computed over the other properties of the tree, they can be re-produced by knowing just their definition. For the same reason, if you provide a computed value in a snapshot you'll end up with an error when you attempt to apply it. ## Model views You may need to use the list of `todos` filtered by completion in various locations of your application. Even if accessing the list of `todos` and filtering them every time may look like a viable solution, if the filter logic is complex or changes over time you'll find out that it's not a viable solution. MST solves that by providing the ability to declare model views. A model's `.views` is declared as a function over the properties (first argument) of the model declaration. Model views can accept parameters and only read data from our store. If you try to change your store from a model view, MST will throw an error and prevent you from doing so. ```javascript const RootStore = types .model({ users: types.map(User), todos: types.map(Todo) }) .views((self) => ({ get pendingCount() { return Array.from(self.todos.values()).filter((todo) => !todo.done).length }, get completedCount() { return Array.from(self.todos.values()).filter((todo) => todo.done).length }, getTodosWhereDoneIs(done) { return Array.from(self.todos.values()).filter((todo) => todo.done === done) } })) .actions((self) => ({ addTodo(id, name) { self.todos.set(id, Todo.create({ name })) } })) ``` [View sample in the playground](https://codesandbox.io/s/x293k4q95o) Notice that other views and View components may call `getTodosWhereDoneIs` outside of the store definition. ## Going further: References Ok, the basics of our TODO application are done! But as I said when starting this tutorial, we want to be able to provide assignees for each of our TODOs! We will focus on this feature; to do that let's assume that the list of users comes from an XHR request or another data source. Feel free to either implement it or add to the TODO application a user management feature. First, we need to populate the `users` map. To do so, we will simply pass in some users when creating the `users` map. ```javascript const store = RootStore.create({ users: { 1: { name: "mweststrate" }, 2: { name: "mattiamanzati" }, 3: { name: "johndoe" } }, todos: { 1: { name: "Eat a cake", done: true } } }) ``` [View sample in the playground](https://codesandbox.io/s/7wwn0x4xkq) Now we need to change our `Todo` model to store the user assigned to the TODO. You could do that by storing the `User` map `id`, and provide a computed that resolves to the user (you can do it as an exercise), but you would end up with a copious amount of code. MST supports references out of the box. That means that we can define a `user` attribute on the `Todo` model that's a reference to a `User` instance. When getting the snapshot, the value of that attribute will be the identifier of the `User`, when reading, it will resolve to the correct instance of the `User` model and when setting you could provide either the `User` model instance or the `User` identifier. ### Identifiers In order to make our reference work, we need to tell MST which attribute to use as a unique identifier of each `User` model instance. The identifier attribute cannot be mutated once the model instance has been created. That also means that if you try to apply a snapshot with a different identifier on that model, it will throw an error. On the other hand, providing an identifier helps MST understand elements in maps and arrays, and allows it to correctly reuse model instances in arrays and maps when possible. To define an identifier, you will need to define a property using the `types.identifier` type composer. For example, we want the identifier to be a string. ```javascript const User = types.model({ id: types.identifier, name: types.optional(types.string, "") }) ``` As I said before, identifiers are required upon creation of the element and cannot be mutated, so if you end up receiving an error like this, it's because you also have to provide ids for the users in the snapshot for the `.create` of `RootStore`. ``` Error: [mobx-state-tree] Error while converting `{"users":{"1":{"name":"mweststrate"},"2":{"name":"mattiamanzati"},"3":{"name":"johndoe"}},"todos":{"1":{"name":"Eat a cake","done":true}}}` to `AnonymousModel`: at path "/users/1/id" value `undefined` is not assignable to type: `identifier(string)`, expected an instance of `identifier(string)` or a snapshot like `identifier(string)` instead. at path "/users/2/id" value `undefined` is not assignable to type: `identifier(string)`, expected an instance of `identifier(string)` or a snapshot like `identifier(string)` instead. at path "/users/3/id" value `undefined` is not assignable to type: `identifier(string)`, expected an instance of `identifier(string)` or a snapshot like `identifier(string)` instead. ``` We can easily fix that by providing a correct snapshot. ```javascript const store = RootStore.create({ users: { 1: { id: "1", name: "mweststrate" }, 2: { id: "2", name: "mattiamanzati" }, 3: { id: "3", name: "johndoe" } }, todos: { 1: { name: "Eat a cake", done: true } } }) ``` [View sample in the playground](https://codesandbox.io/s/44jn3pv2x) ### How to define the reference The reference we are looking for can be easily defined as `types.reference(User)`. Sometimes this can lead to circular references that may use a model before it's declared. To postpone the resolution of the model, you can use `types.late(() => User)` instead of just `User` and that will hoist the model and defer its evaluation. The `user` assignee for the `Todo` could also be omitted, so we will use `types.maybe(...)` to allow the `user` property to be `null` and be initialized as `null`. ```javascript const Todo = types .model({ name: types.optional(types.string, ""), done: types.optional(types.boolean, false), user: types.maybe(types.reference(types.late(() => User))) }) .actions((self) => ({ setName(newName) { self.name = newName }, toggle() { self.done = !self.done } })) ``` [View sample in the playground](https://codesandbox.io/s/xv1lkqw9oq) ### Setting a reference value The reference value can be set by providing either the identifier or a model instance. First of all, we need to define an action that will allow you to change the `user` of the `Todo`. ```javascript const Todo = types .model({ name: types.optional(types.string, ""), done: types.optional(types.boolean, false), user: types.maybe(types.reference(types.late(() => User))) }) .actions((self) => ({ setName(newName) { self.name = newName }, setUser(user) { if (user === "") { // When selected value is empty, set as undefined self.user = undefined } else { self.user = user } }, toggle() { self.done = !self.done } })) ``` Now we need to edit our views to display a select along with each `TodoView`, where the user can choose the assignee for that task. To do so, we will create a separate component `UserPickerView` and use it inside the `TodoView` component to trigger the `setUser` call. That's it! ```javascript const UserPickerView = observer((props) => ( )) const TodoView = observer((props) => (
props.todo.toggle()} /> props.todo.setName(e.target.value)} /> props.todo.setUser(userId)} />
)) const TodoCounterView = observer((props) => (
{props.store.pendingCount} pending, {props.store.completedCount} completed
)) const AppView = observer((props) => (
{Array.from(props.store.todos.values()).map((todo) => ( ))}
)) ``` [View sample in the playground](https://codesandbox.io/s/6j3qy74kpw) ## References are safe! One neat feature of references, is that they will throw an error if you accidentally remove a model that is required by a computed property! If you try to remove a user that's used by a reference, you'll get something like this: ``` [mobx-state-tree] Failed to resolve reference of type : '1' (in: /todos/1/user) ``` ================================================ FILE: docs/intro/installation.md ================================================ --- id: installation title: Installation ---
- NPM: `npm install mobx mobx-state-tree --save` - Yarn: `yarn add mobx mobx-state-tree` - CDN: https://unpkg.com/mobx-state-tree/dist/mobx-state-tree.umd.js (exposed as `window.mobxStateTree`) - CodeSandbox [TodoList demo](https://codesandbox.io/s/y64pzxj01) fork for testing and bug reporting TypeScript typings are included in the packages. Use `module: "commonjs"` or `moduleResolution: "node"` to make sure they are picked up automatically in any consuming project. Supported environments: - MobX-State-Tree 4+ runs in any JavaScript environment, including browsers, Node, React Native (including Hermes), and more Supported devtools: - [Reactotron](https://github.com/infinitered/reactotron) - [MobX DevTools](https://chrome.google.com/webstore/detail/mobx-developer-tools/pfgnfdagidkfgccljigdamigbcnndkod) - The Redux DevTools can be connected as well as demonstrated [here](https://github.com/coolsoftwaretyler/mst-example-redux-todomvc/blob/main/src/index.js#L6) ================================================ FILE: docs/intro/philosophy.md ================================================ --- id: philosophy title: Overview & Philosophy ---
`mobx-state-tree` (also known as "MST") is a state container that combines the _simplicity and ease of mutable data_ with the _traceability of immutable data_ and the _reactiveness and performance of observable data_. Simply put, MST tries to combine the best features of both immutability (transactionality, traceability and composition) and mutability (discoverability, co-location and encapsulation) based approaches to state management; everything to provide the best developer experience possible. Unlike MobX itself, MST is very opinionated about how data should be structured and updated. This makes it possible to solve many common problems out of the box. Central in MST is the concept of a _living tree_. The tree consists of mutable, but strictly protected objects enriched with _runtime type information_. In other words, each tree has a _shape_ (type information) and _state_ (data). From this living tree, immutable, structurally shared, snapshots are automatically generated. ```javascript import { types, onSnapshot } from "mobx-state-tree" const Todo = types .model("Todo", { title: types.string, done: false }) .actions((self) => ({ toggle() { self.done = !self.done } })) const Store = types.model("Store", { todos: types.array(Todo) }) // create an instance from a snapshot const store = Store.create({ todos: [ { title: "Get coffee" } ] }) // listen to new snapshots onSnapshot(store, (snapshot) => { console.dir(snapshot) }) // invoke action that modifies the tree store.todos[0].toggle() // prints: `{ todos: [{ title: "Get coffee", done: true }]}` ``` By using the type information available, snapshots can be converted to living trees, and vice versa, with zero effort. Because of this, [time travelling](https://github.com/coolsoftwaretyler/mst-example-boxes/blob/main/src/stores/time.js) is supported out of the box, and tools like HMR are trivial to support, see this [HMR example](https://github.com/coolsoftwaretyler/mst-example-boxes/blob/main/src/stores/domain-state.js#L116-L126). The type information is designed in such a way that it is used both at design- and run-time to verify type correctness (Design time type checking works in TypeScript only at the moment; Flow PR's are welcome!) ``` [mobx-state-tree] Value '{\"todos\":[{\"turtle\":\"Get tea\"}]}' is not assignable to type: Store, expected an instance of Store or a snapshot like '{ todos: { title: string; done: boolean }[] }' instead. ``` _Runtime type error_ ![typescript error](/img/tserror.png) _Designtime type error_ Because state trees are living, mutable models, actions are straight-forward to write; just modify local instance properties where appropriate. See the `toggle()`-action in the Todo-store above or the examples below. It is not necessary to produce a new state tree yourself, MST's snapshot functionality will derive one for you automatically. Although mutable sounds scary to some, fear not, actions have many interesting properties. By default trees can only be modified by using an action that belongs to the same subtree. Furthermore, actions are replayable and can be used to distribute changes ([example](https://github.com/coolsoftwaretyler/mst-example-boxes/blob/main/src/stores/time.js)). Moreover, because changes can be detected on a fine grained level, JSON patches are supported out of the box. Simply subscribing to the patch stream of a tree is another way to sync diffs with, for example, back-end servers or other clients ([example](https://github.com/coolsoftwaretyler/mst-example-boxes/blob/main/src/stores/socket.js)). ![patches](/img/patches.png) Since MST uses MobX behind the scenes, it integrates seamlessly with [mobx](https://mobx.js.org) and [mobx-react-lite (or mobx-react)](https://mobx.js.org/react-integration.html). See also this [egghead.io lesson: Render mobx-state-tree Models in React](https://egghead.io/lessons/react-render-mobx-state-tree-models-in-react). Even cooler, because it supports snapshots, middleware and replayable actions out of the box, it is possible to replace a Redux store and reducer with a MobX state tree. This makes it possible to connect the Redux devtools to MST. See the [Redux / MST TodoMVC example](https://github.com/coolsoftwaretyler/mst-example-redux-todomvc/blob/main/src/index.js#L6). --- For futher reading: the conceptual difference between snapshots, patches and actions in relation to distributing state changes is extensively discussed in this [blog post](https://medium.com/@mweststrate/distributing-state-changes-using-snapshots-patches-and-actions-part-1-2811a2fcd65f) ![devtools](/img/reduxdevtools.png) Finally, MST has built-in support for references, identifiers, dependency injection, change recording and circular type definitions (even across files). Even fancier, it analyses liveliness of objects, failing early when you try to access accidentally cached information! (More on that later) A unique feature of MST is that it offers liveliness guarantees. MST will throw an exception when reading or writing from objects that are no longer part of a state tree. This protects you against accidental stale reads of objects still referred by, for example, a closure. ```javascript const oldTodo = store.todos[0] store.removeTodo(0) function logTodo(todo) { setTimeout(() => console.log(todo.title), 1000) } logTodo(store.todos[0]) store.removeTodo(0) // throws exception in one second for using an stale object! ``` Despite all that, you will see that in practice the API is quite straightforward! --- Another way to look at mobx-state-tree is to consider it, as argued by Daniel Earwicker, to be ["React, but for data"](http://danielearwicker.github.io/json_mobx_Like_React_but_for_Data_Part_2.html). Like React, MST consists of composable components, called _models_, which captures a small piece of state. They are instantiated from props (snapshots) and after that manage and protect their own internal state (using actions). Moreover, when applying snapshots, tree nodes are reconciled as much as possible. There is even a context-like mechanism, called environments, to pass information to deep descendants. An introduction to the philosophy can be watched [here](https://youtu.be/ta8QKmNRXZM?t=21m52s). [Slides](https://immer-mutable-state.surge.sh/). Or, as [markdown](https://github.com/mweststrate/reactive2016-slides/blob/master/slides.md) to read it quickly. mobx-state-tree "immutable trees" and "graph model" features talk, ["Next Generation State Management"](https://www.youtube.com/watch?v=rwqwwn_46kA) at React Europe 2017. [Slides](http://tree.surge.sh/#1). ================================================ FILE: docs/intro/welcome.md ================================================ --- id: welcome title: Welcome to MobX-State-Tree! ---
**_Full-featured reactive state management without the boilerplate._** ## What is MobX-State-Tree? MobX-State-Tree (MST) is a [batteries included]() state management library. It only requires **one peer dependency**, and will provide you with: 1. **Centralized stores** for your data 2. **Mutable, but protected data**, which means it is easy to work with your data, but safe to modify. 3. **Serializable and traceable updates**. The mutable, protected nature of MobX-State-Tree data means you can **generate snapshots** and do **time-travel debugging**. 4. **Side effect management**, so you don't need to write `useEffect` hooks or their equivalent to manage the consequences of data mutations. You can do it all from MST itself. 5. **Runtime type checking**, so you can't accidentally assign the wrong data type to a property 6. **Static type checking** with TypeScript inference from your runtime types - automatically! 7. **Data normalization** - MST has support for references, so you can normalize data across your application code. 8. **Warm, welcoming community**. We pride ourselves on a healthy and kind open source community. ## Basic Example Here's what MST code looks like: _You can play with it in [this CodeSandbox playground](https://codesandbox.io/s/boring-pond-cmooq?file=/src/index.js)._ ```javascript import { t, onSnapshot } from "mobx-state-tree" // A tweet has a body (which is text) and whether it's read or not const Tweet = t .model("Tweet", { body: t.string, read: false // automatically inferred as type "boolean" with default "false" }) .actions((tweet) => ({ toggle() { tweet.read = !tweet.read } })) // Define the Twitter "store" as having an array of tweets const TwitterStore = t.model("TwitterStore", { tweets: t.array(Tweet) }) // create your new Twitter store instance with some initial data const twitterStore = TwitterStore.create({ tweets: [ { body: "Anyone tried MST?" } ] }) // Listen to new snapshots, which are created anytime something changes onSnapshot(twitterStore, (snapshot) => { console.log(snapshot) }) // Let's mark the first tweet as "read" by invoking the "toggle" action twitterStore.tweets[0].toggle() // In the console, you should see the result: `{ tweets: [{ body: "Anyone tried MST?", read: true }]}` ``` ## Video Demonstration Jamon Holmgren has an excellent introduction video with a more realistic, robust example of MobX-State-Tree and React. Check it out! ## MobX Ecosystem [MobX](https://github.com/mobxjs/mobx) is [one of the most popular Redux alternatives](https://2019.stateofjs.com/data-layer/mobx/) and is used (along with MobX-State-Tree) by companies all over the world, including Netflix, Grow, IBM, DAZN, Baidu, and more. If you're wondering how MobX-State-Tree is distinct from MobX, you can think of it like this: **MobX is a state management "engine", and MobX-State-Tree is a luxury car**. MST gives you the structure, tools, and other features to get you where you're going. MST is valuable in a large team but also useful in smaller applications when you expect your code to scale rapidly. And if we compare it to Redux, MST offers better performance with much less boilerplate code than Redux! Since MST uses MobX under the hood, MobX-State-Tree works with the MobX bindings for React, React Native, Vue, Angular, Svelte, and even barebones JavaScript apps. _You don't need to know how to use MobX in order to use MST._ Just like you don't need to know how your car's engine works to be an excellent driver. It can help, but it's not necessary. ## Next Steps - Learn how to [install MobX-State-Tree](./installation.md) or jump straight to our [Getting Started](./getting-started.md) guide! - View [examples](./examples.md) here. - If you're interested in the philosophy behind MobX-State-Tree and a lot more explanation of features and benefits, check out the [Philosophy](./philosophy.md) page. - Or check out a talk or two on our [Resources](./../tips/resources.md) page ================================================ FILE: docs/overview/hooks.md ================================================ --- id: hooks title: Lifecycle hooks overview ---
egghead.io lesson 14: Loading Data from the Server after model creation
Hosted on egghead.io
`mobx-state-tree` supports passing a variety of hooks that are called throughout a node's lifecycle. Hooks are passes as actions with the name of the hook, like: ```javascript const Todo = types.model("Todo", { done: true }).actions((self) => ({ afterCreate() { console.log("Created a new todo!") } })) ``` | Hook | Meaning | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `afterCreate` | Immediately after an instance is created and initial values are applied. Children will fire this event before parents. You can't make assumptions about the parent safely, use `afterAttach` if you need to. | | `afterAttach` | As soon as the _direct_ parent is assigned (this node is attached to another node). If an element is created as part of a parent, `afterAttach` is also fired. Unlike `afterCreate`, `afterAttach` will fire breadth first. So, in `afterAttach` one can safely make assumptions about the parent, but in `afterCreate` not | | `beforeDetach` | As soon as the node is removed from the _direct_ parent, but only if the node is _not_ destroyed. In other words, when `detach(node)` is used | | `beforeDestroy` | Called before the node is destroyed, as a result of calling `destroy`, or by removing or replacing the node from the tree. Child destructors will fire before parents | | `preProcessSnapshot` | Deprecated, prefer `types.snapshotProcessor`. Before creating an instance or applying a snapshot to an existing instance, this hook is called to give the option to transform the snapshot before it is applied. The hook should be a _pure_ function that returns a new snapshot. This can be useful to do some data conversion, enrichment, property renames, etc. This hook is not called for individual property updates. _\*\*Note 1: Unlike the other hooks, this one is \_not_ created as part of the `actions` initializer, but directly on the type!**\_ \_**Note 2: The `preProcessSnapshot` transformation must be pure; it should not modify its original input argument!\*\*\_ | | `postProcessSnapshot` | Deprecated, prefer `types.snapshotProcessor`. This hook is called every time a new snapshot is being generated. Typically it is the inverse function of `preProcessSnapshot`. This function should be a pure function that returns a new snapshot. _\*\*Note: Unlike the other hooks, this one is \_not_ created as part of the `actions` initializer, but directly on the type!\*\*\_ | All hooks can be defined multiple times and can be composed automatically. ## Lifecycle hooks for `types.array`/`types.map` Hooks for `types.array`/`types.map` can be defined by using the `.hooks(self => ({}))` method. Calling `.hooks(...)` produces new type, same as calling `.actions()` for `types.model`. Available hooks are: | Hook | Meaning | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `afterCreate` | Immediately after an instance is initialized: right after `.create()` for root node or after the first access for the nested one. Children will fire this event before parents. You can't make assumptions about the parent safely, use `afterAttach` if you need to. | | `afterAttach` | As soon as the _direct_ parent is assigned (this node is attached to another node). If an element is created as part of a parent, `afterAttach` is also fired. Unlike `afterCreate`, `afterAttach` will fire breadth first. So, in `afterAttach` one can safely make assumptions about the parent, but in `afterCreate` not | | `beforeDetach` | As soon as the node is removed from the _direct_ parent, but only if the node is _not_ destroyed. In other words, when `detach(node)` is used | | `beforeDestroy` | Called before the node is destroyed, as a result of calling `destroy`, or by removing or replacing the node from the tree. Child destructors will fire before parents | ### Snapshot processing hooks You can also modify snapshots as they are generated from your nodes, or applied to your nodes with `types.snapshotProcessor`. This type wraps an existing type and allows defining custom hooks for snapshot modifications. For example, you can wrap an existing model in a snapshot processor which transforms a snapshot from the server into the shape your model expects with `preProcess`: ```javascript const TodoModel = types.model("Todo", { done: types.boolean, }); const Todo = types.snapshotProcessor(TodoModel, { preProcessor(snapshot) { return { // auto convert strings to booleans as part of preprocessing done: snapshot.done === "true" ? true : snapshot.done === "false" ? false : snapshot.done } }); const todo = Todo.create({ done: "true" }) // snapshot will be transformed on the way in ``` Snapshots can also be transformed from the base shape generated by `mobx-quick-tree` using the `postProcess` hook. For example, we can format a date object in the snapshot with a specific date format that a backend might accept: ```javascript const TodoModel = types.model("Todo", { done: types.boolean, createdAt: types.Date }); const Todo = types.snapshotProcessor(TodoModel, { postProcessor(snapshot, node) { return { ...snapshot, createdAt: node.createdAt.getTime() } }); const todo = Todo.create({done: true, createdAt: new Date()}); const snapshot = getSnapshot(todo); // { done: true, createdAt: 1699504649386 } ``` | Hook | Meaning | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `preProcessor(inputSnapshot)` | Transform a snapshot before it is applied to a node. The output snapshot must be valid for application to the wrapped type. The `preProcess` hook is passed the input snapshot, but not passed the node, as it is not done being constructed yet, and not attached to the tree. If you need to modify the node in the context of the tree, use the `afterCreate` hook. | | `postProcessor(outputSnapshot, node)` | Transform a snapshot after it has been generated from a node. The transformed value will be returned by `getSnapshot`. The `postProcess` hook is passed the initial outputSnapshot, as well as the instance object the snapshot has been generated for. It is safe to access properties of the node or other nodes when post processing snapshots. | #### When to use snapshot hooks `preProcess` and `postProcess` hooks should be used to convert your data into types that are more acceptable to MST. Snapshots are often JSON serialized, so if you need to use richly typed objects like `URL`s or `Date`s that can't be JSON serialized, you can use snapshot processors to convert to and from the serialized form. Typically, it should be the case that `postProcessor(preProcessor(snapshot)) === snapshot`. If your snapshot processor hooks are non-deterministic, or rely on state beyond just the base snapshot, it's easy to introduce subtle bugs and is best avoided. If you are considering adding a snapshot processor that is non-deterministic or relies on other state, consider using a dedicated property or view that produces the same information. Like snapshots, properties and views are observable and memoized, but they don't need to have an inverse for serializing back to a snapshot. For example, if you want to capture the current time a snapshot was generated, you may be tempted to use a snapshot processor: ```javascript const TodoModel = types.model("Todo", { done: types.boolean, }); const Todo = types.snapshotProcessor(TodoModel, { // discouraged, try not to do this postProcessor(snapshot, node) { return { ...snapshot, createdAt: new Date().toISOString(); } }); const todo = Todo.create({ done: false }) getSnapshot(todo) // will have a `createdAt property` ``` Instead, this data could be better represented as a property right on the model, which is included in the snapshot by default: ```javascript const Todo = types.model("Todo", { done: types.boolean, createdAt: types.optional(types.Date, () => new Date()) }); const todo = Todo.create({ done: false }) getSnapshot(todo) // will also have a `createdAt property` ``` Advanced use-cases that require impure or otherwise inconsistent snapshot processors are however supported by MST. ================================================ FILE: docs/overview/types.md ================================================ --- id: types title: Types overview ---
egghead.io lesson 11: More mobx-state-tree Types: map, literal, union, and enumeration
Hosted on egghead.io
egghead.io lesson 17: Create Dynamic Types and use Type Composition to Extract Common Functionality
Hosted on egghead.io
These are the types available in MST. All types can be found in the `types` namespace, e.g. `types.string`. ## Complex types - [`types.model(properties, actions)`](/API/#model) Defines a "class like" type with properties and actions to operate on the object. - [`types.array(type)`](/API/#array) Declares an array of the specified type. - [`types.map(type)`](/API/#map) Declares a map of the specified type. Note that since MST v3 `types.array` and `types.map` are wrapped in `types.optional` by default, with `[]` and `{}` set as their default values, respectively. ## Primitive types - [`types.string`](/API/#const-string) - [`types.number`](/API/#const-number) - [`types.integer`](/API/#const-integer) - [`types.float`](/API/#const-float) - [`types.finite`](/API/#const-finite) - [`types.boolean`](/API/#const-boolean) - [`types.Date`](/API/#const-dateprimitive) - [`types.custom`](/API/#custom) creates a custom primitive type. This is useful to define your own types that map a serialized form one-to-one to an immutable object like a Decimal or Date. ## Utility types - [`types.union(options?: { dispatcher?: (snapshot) => Type, eager?: boolean }, types...)`](/API/#union) create a union of multiple types. If the correct type cannot be inferred unambiguously from a snapshot, provide a dispatcher function to determine the type. When `eager` flag is set to `true` (default) - the first matching type will be used, if set to `false` the type check will pass only if exactly 1 type matches. - [`types.optional(type, defaultValue, optionalValues?)`](/API/#optional) marks a value as being optional (in e.g. a model). If a value is not provided/`undefined` (or set to any of the primitive values passed as an optional `optionalValues` array) the `defaultValue` will be used instead. If `defaultValue` is a function, it will be evaluated. This can be used to generate, for example, IDs or timestamps upon creation. - [`types.literal(value)`](/API/#literal) can be used to create a literal type, where the only possible value is specifically that value. This is very powerful in combination with `union`s. E.g. `temperature: types.union(types.literal("hot"), types.literal("cold"))`. - [`types.enumeration(name?, options: string[])`](/API/#enumeration) creates an enumeration. This method is a shorthand for a union of string literals. If you are using typescript and want to create a type based on an string enum (e.g. `enum Color { ... }`) then use `types.enumeration("Color", Object.values(Color))`, where the `"Color"` name argument is optional. - [`types.refinement(name?, baseType, (snapshot) => boolean)`](/API/#refinement) creates a type that is more specific than the base type, e.g. `types.refinement(types.string, value => value.length > 5)` to create a type of strings that can only be longer than 5. - [`types.maybe(type)`](/API/#maybe) makes a type optional and nullable. The value `undefined` will be used to represent nullability. Shorthand for `types.optional(types.union(type, types.literal(undefined)), undefined)`. - [`types.maybeNull(type)`](/API/#maybenull) like `maybe`, but uses `null` to represent the absence of a value. - [`types.null`](/API/#const-nulltype) the type of `null`. - [`types.undefined`](/API/#const-undefinedtype) the type of `undefined`. - [`types.late(() => type)`](/API/#late) can be used to create recursive or circular types, or types that are spread over files in such a way that circular dependencies between files would be an issue otherwise. - [`types.frozen(subType? | defaultValue?)`](/API/#frozen) Accepts any kind of serializable value (both primitive and complex), but assumes that the value itself is **immutable** and **serializable**. `frozen` can be invoked in a few different ways: - `types.frozen()` - behaves the same as types.frozen in MST 2. - `types.frozen(subType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type. Note that the type will not actually be instantiated, so it can only be used to check the shape of the data. Adding views or actions to SubType would be pointless. - `types.frozen(someDefaultValue)` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field - (Typescript) `types.frozen(...)` - provide a typescript type, to help in strongly typing the field (design time only) - [`types.compose(name?, type1...typeX)`](/API/#compose), creates a new model type by taking a bunch of existing types and combining them into a new one. - [`types.reference(targetType)`](/API/#reference) creates a property that is a reference to another item of the given `targetType` somewhere in the same tree. See [references](/concepts/references#references) for more details. - [`types.safeReference(targetType)`](/API/#safereference) is like a standard reference, except that it accepts the undefined value by default and automatically sets itself to undefined (when the parent is a model) / removes itself from arrays and maps when the reference it is pointing to gets detached/destroyed. See [references](/concepts/references#references) for more details. - [`types.snapshotProcessor(type, processors, name?)`](/API/#snapshotprocessor) runs a pre snapshot / post snapshot processor before/after serializing a given type. [See known issue with `applySnapshot` and `preProcessSnapshot`](https://github.com/mobxjs/mobx-state-tree/issues/1317) Example: ```ts const Todo1 = types.model({ text: types.string }) // in the backend the text type must be null when empty interface BackendTodo { text: string | null } const Todo2 = types.snapshotProcessor(Todo1, { // from snapshot to instance preProcessor(sn: BackendTodo) { return { text: sn.text || ""; } }, // from instance to snapshot postProcessor(sn): BackendTodo { return { text: !sn.text ? null : sn.text } } }) ``` ## Property types Property types can only be used as a direct member of a `types.model` type and not further composed (for now). - [`types.identifier`](/API/#const-identifier) Only one such member can exist in a `types.model` and should uniquely identify the object. See [identifiers](/concepts/references#identifiers) for more details. - [`types.identifierNumber`](/API/#const-identifiernumber) Similar to `types.identifier`. However, during serialization, the identifier value will be parsed from / serialized to a number. ================================================ FILE: docs/overview/utilties.md ================================================ --- id: api title: API overview ---
See the [TypeDocs](/API/) for full details and typings. | signature | | | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addDisposer(node, () => void)`](/API/#adddisposer) | Add a function to be invoked whenever the target node is about to be destroyed | | [`addMiddleware(node, middleware: (actionDescription, next) => any, includeHooks)`](/API/#addmiddleware) | Attaches middleware to a node. See [middleware](../concepts/middleware). Returns disposer. | | [`applyAction(node, actionDescription)`](/API/#applyaction) | Replays an action on the targeted node | | [`applyPatch(node, jsonPatch)`](/API/#applypatch) | Applies a JSON patch, or array of patches, to a node in the tree | | [`applySnapshot(node, snapshot)`](/API/#applysnapshot) | Updates a node with the given snapshot | | [`cast(nodeOrSnapshot)`](/API/#cast) | Cast a node instance or snapshot to a node instance so it can be used in assignment operations | | [`castToSnapshot(nodeOrSnapshot)`](/API/#casttosnapshot) | Cast a node instance to a snapshot so it can be used inside create operations | | [`castToReferenceSnapshot(node)`](/API/#casttoreferencesnapshot) | Cast a node instance to a reference snapshot so it can be used inside create operations | | [`createActionTrackingMiddleware`](/API/#createactiontrackingmiddleware) | Utility to make writing middleware that tracks async actions less cumbersome. Consider migrating to `createActionTrackingMiddleware2` | | [`createActionTrackingMiddleware2`](/API/#createactiontrackingmiddleware) | Utility to make writing middleware that tracks async actions less cumbersome | | [`clone(node, keepEnvironment?: boolean / newEnvironment)`](/API/#clone) | Creates a full clone of the given node. By default preserves the same environment | | [`decorate(handler, function)`](/API/#decorate) | Attaches middleware to a specific action (or flow) | | [`destroy(node)`](/API/#destroy) | Kills `node`, making it unusable. Removes it from any parent in the process | | [`detach(node)`](/API/#detach) | Removes `node` from its current parent, and lets it live on as standalone tree | | [`flow(generator)`](/API/#flow) | Creates an asynchronous flow based on a generator function | | [`castFlowReturn(value)`](/API/#castflowreturn) | Casts a flow return value so it can be correctly inferred as return type. Only needed when using TypeScript and when returning a Promise. | | [`getChildType(node, property?)`](/API/#getchildtype) | Returns the declared type of the given `property` of `node`. For arrays and maps `property` can be omitted as they all have the same type | | [`getEnv(node)`](/API/#getenv) | Returns the environment of `node`, see [dependency injection](../concepts/dependency-injection) | | [`getParent(node, depth=1)`](/API/#getparent) | Returns the intermediate parent of the `node`, or a higher one if `depth > 1` | | [`getParentOfType(node, type)`](/API/#getparentoftype) | Return the first parent that satisfies the provided type | | [`getPath(node)`](/API/#getpath) | Returns the path of `node` in the tree | | [`getPathParts(node)`](/API/#getpathparts) | Returns the path of `node` in the tree, unescaped as separate parts | | [`getRelativePath(base, target)`](/API/#getrelativepath) | Returns the short path, which one could use to walk from node `base` to node `target`, assuming they are in the same tree. Up is represented as `../` | | [`getRoot(node)`](/API/#getroot) | Returns the root element of the tree containing `node` | | [`getIdentifier(node)`](/API/#getidentifier) | Returns the identifier of the given element | | [`getNodeId(node)`](/API/#getnodeid) | Returns the unique node id (not to be confused with the instance identifier) for a given instance | | [`getSnapshot(node, applyPostProcess)`](/API/#getsnapshot) | Returns the snapshot of the `node`. See [snapshots](../concepts/snapshots) | | [`getType(node)`](/API/#gettype) | Returns the type of `node` | | [`hasParent(node, depth=1)`](/API/#hasparent) | Returns `true` if `node` has a parent at `depth` | | [`hasParentOfType(node, type)`](/API/#hasparentoftype) | Returns `true` if the `node` has a parent that satisfies the provided type | | [`isAlive(node)`](/API/#isalive) | Returns `true` if `node` is alive | | [`isStateTreeNode(value)`](/API/#isstatetreenode) | Returns `true` if `value` is a node of a mobx-state-tree | | [`isProtected(value)`](/API/#isprotected) | Returns `true` if the given node is protected, see [actions](../concepts/actions) | | [`isValidReference(() => node / null / undefined, checkIfAlive = true)`](/API/#isvalidreference) | Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns if the check passes or not. | | [`isRoot(node)`](/API/#isroot) | Returns true if `node` has no parents | | [`joinJsonPath(parts)`](/API/#joinjsonpath) | Joins and escapes the given path `parts` into a JSON path | | [`onAction(node, (actionDescription) => void)`](/API/#onaction) | A built-in middleware that calls the provided callback with an action description upon each invocation. Returns disposer | | [`onPatch(node, (patch) => void)`](/API/#onpatch) | Attach a JSONPatch listener, that is invoked for each change in the tree. Returns disposer | | [`onSnapshot(node, (snapshot) => void)`](/API/#onsnapshot) | Attach a snapshot listener, that is invoked for each change in the tree. Returns disposer | | [`process(generator)`](/API/#process) | `DEPRECATED` – replaced with [flow](/API/#flow) | | [`protect(node)`](/API/#protect) | Protects an unprotected tree against modifications from outside actions | | [`recordActions(node)`](/API/#recordactions) | Creates a recorder that listens to all actions in `node`. Call `.stop()` on the recorder to stop this, and `.replay(target)` to replay the recorded actions on another tree | | [`recordPatches(node)`](/API/#recordpatches) | Creates a recorder that listens to all patches emitted by the node. Call `.stop()` on the recorder to stop this, and `.replay(target)` to replay the recorded patches on another tree | | [`getMembers(node)`](/API/#getMembers) | Returns the model name, properties, actions, views, volatiles of a model node instance | | [`getPropertyMembers(typeOrNode)`](/API/#getPropertyMembers) | Returns the model name and properties of a model type for either a model type or a model node | | [`resolve(node, path)`](/API/#resolve) | Resolves a `path` (json path) relatively to the given `node` | | [`resolveIdentifier(type, target, identifier)`](/API/#resolveidentifier) | resolves an identifier of a given type in a model tree | | [`resolvePath(target, path)`](/API/#resolvepath) | resolves a JSON path, starting at the specified target | | [`setLivelinessChecking("warn" / "ignore" / "error")`](/API/#setlivelinesschecking) | Defines what MST should do when running into reads / writes to objects that have died. By default it will print a warning. Use te `"error"` option to easy debugging to see where the error was thrown and when the offending read / write took place | | [`getLivelinessChecking()`](/API/#getlivelinesschecking) | Returns the current liveliness checking mode. | | [`splitJsonPath(path)`](/API/#splitjsonpath) | Splits and unescapes the given JSON `path` into path parts | | [`typecheck(type, value)`](/API/#typecheck) | Typechecks a value against a type. Throws on errors. Use this if you need typechecks even in a production build. NOTE: set process.env.ENABLE_TYPE_CHECK = "true" if you want to enable type checking in any environment | | [`tryResolve(node, path)`](/API/#tryresolve) | Like `resolve`, but just returns `null` if resolving fails at any point in the path | | [`tryReference(() => node / null / undefined, checkIfAlive = true)`](/API/#tryreference) | Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns such reference if it the check passes, else it returns undefined. | | [`unprotect(node)`](/API/#unprotect) | Unprotects `node`, making it possible to directly modify any value in the subtree, without actions | | [`walk(startNode, (node) => void)`](/API/#walk) | Performs a depth-first walk through a tree | | [`escapeJsonPath(path)`](/API/#escapejsonpath) | escape special characters in an identifier, according to http://tools.ietf.org/html/rfc6901 | | [`unescapeJsonPath(path)`](/API/#unescapejsonpath) | escape special characters in an identifier, according to http://tools.ietf.org/html/rfc6901 | | [`isType(value)`](/API/#isType) | Returns if a given value represents a type. | | [`isArrayType(value)`](/API/#isArrayType) | Returns if a given value represents an array type. | | [`isFrozenType(value)`](/API/#isFrozenType) | Returns if a given value represents a frozen type. | | [`isIdentifierType(value)`](/API/#isIdentifierType) | Returns if a given value represents an identifier type. | | [`isLateType(value)`](/API/#isLateType) | Returns if a given value represents a late type. | | [`isLiteralType(value)`](/API/#isLiteralType) | Returns if a given value represents a literal type. | | [`isMapType(value)`](/API/#isMapType) | Returns if a given value represents a map type. | | [`isModelType(value)`](/API/#isModelType) | Returns if a given value represents a model type. | | [`isOptionalType(value)`](/API/#isOptionalType) | Returns if a given value represents an optional type. | | [`isPrimitiveType(value)`](/API/#isPrimitiveType) | Returns if a given value represents a primitive type. | | [`isReferenceType(value)`](/API/#isReferenceType) | Returns if a given value represents a reference type. | | [`isRefinementType(value)`](/API/#isRefinementType) | Returns if a given value represents a refinement type. | | [`isUnionType(value)`](/API/#isUnionType) | Returns if a given value represents a union type. | | [`getRunningActionContext()`](/API/#getrunningactioncontext) | Returns the currently executing MST action context, or undefined if none. | | [`isActionContextChildOf(actionContext, parent)`](/API/#isActionContextChildOf) | Returns if the given action context is a parent of this action context. | | [`isActionContextThisOrChildOf(actionContext, parentOrSame)`](/API/#isActionContextThisOrChildOf) | Returns if the given action context is this or a parent of this action context. | A _disposer_ is a function that cancels the effect for which it was created. ================================================ FILE: docs/recipes/auto-generated-property-setter-actions.md ================================================ --- id: auto-generated-property-setter-actions title: Auto-Generated Property Setter Actions --- This recipe was [originally developed by Infinite Red](https://shift.infinite.red/a-mobx-state-tree-shortcut-for-setter-actions-ac88353df060). If you want to modify your MobX-State-Tree model properties, you usually have to write one setter per-property. In a model with two fields, that looks like this: ```ts import { types } from "mobx-state-tree" const UserModel = types .model("User", { name: types.string, age: types.number }) .actions((self) => ({ setName(newName: string) { self.name = newName }, setAge(newAge: number) { self.age = newAge } })) ``` As your model grows in size and complexity, these setter actions can be tedious to write, and increase your source file size, making it harder to read through the actual logic of your model. You can write a generic action in your model, like this: ```ts import { types, SnapshotIn } from "mobx-state-tree" const UserModel = types .model("User", { name: types.string, age: types.number }) .actions((self) => ({ setProp, V extends SnapshotIn[K]>( field: K, newValue: V ) { self[field] = newValue } })) const user = UserModel.create({ name: "Jamon", age: 40 }) user.setProp("name", "Joe") // all good! // typescript will error, like it's supposed to user.setProp("age", "shouldn't work") ``` Or, if you want to extract that for easier reuse across different models, you can write a helper, like this: ```ts import { IStateTreeNode, SnapshotIn } from "mobx-state-tree" // This custom type helps TS know what properties can be modified by our returned function. It excludes actions and views, but still correctly infers model properties for auto-complete and type safety. type OnlyProperties = { [K in keyof SnapshotIn]: K extends keyof T ? T[K] : never } /** * If you include this in your model in an action() block just under your props, * it'll allow you to set property values directly while retaining type safety * and also is executed in an action. This is useful because often you find yourself * making a lot of repetitive setter actions that only update one prop. * * E.g.: * * const UserModel = types.model("User") * .props({ * name: types.string, * age: types.number * }) * .actions(withSetPropAction) * * const user = UserModel.create({ name: "Jamon", age: 40 }) * * user.setProp("name", "John") // no type error * user.setProp("age", 30) // no type error * user.setProp("age", "30") // type error -- must be number */ export const withSetPropAction = (mstInstance: T) => ({ setProp, V extends SnapshotIn[K]>(field: K, newValue: V) { ;(mstInstance as T & OnlyProperties)[field] = newValue } }) ``` You can use the helper in a model like so: ```ts import { t } from "mobx-state-tree" import { withSetPropAction } from "./withSetPropAction" const Person = t .model("Person", { name: t.string }) .views((self) => ({ get lowercaseName() { return self.name.toLowerCase() } })) .actions((self) => ({ setName(name: string) { self.name = name } })) .actions(withSetPropAction) const you = Person.create({ name: "your name" }) you.setProp("name", "Another Name") // These will all trigger runtime errors. They are included to demonstrate TS support for // withSetPropAction. try { // @ts-expect-error - this should error because it's the wrong type for name. you.setProp("name", 123) // @ts-expect-error - this should error since 'nah' is not a property. you.setProp("nah", 123) // @ts-expect-error - we cannot set views like we can with properties. you.setProp("lowercaseName", "your name") // @ts-expect-error - we cannot set actions like we can with properties. you.setProp("setName", "your name") } catch (e) { console.error(e) } ``` [See this working in CodeSandbox](https://codesandbox.io/p/sandbox/set-prop-action-ts-fix-p5psk7?file=%2Fsrc%2Findex.ts%3A9%2C23). This is a type-safe way to reduce boilerplate and make your MobX-State-Tree models more readable. ================================================ FILE: docs/recipes/mst-query.md ================================================ --- id: mst-query title: Manage Asynchronous Data with mst-query --- Find the `mst-query` library on GitHub: https://github.com/ConrabOpto/mst-query. # mst-query mst-query is a query library designed specifically for MobX-State-Tree. It functions similarly to [react-query](https://tanstack.com/query/latest) but operates as a thin layer on top of a MobX-State-Tree store. Key features include: * Asynchronous data management with React hooks * Automatic normalization * Query invalidation upon stale data * Imperative api * Optimistic update * Garbage collection In this recipes section, we'll briefly discuss each of these features and how they solve common problems when using MST. ## Async data managment with React hooks Creating your own React hook for data fetching in components can be challenging. Managing all potential edge cases that may arise is both complex and error-prone. Opting for a third-party hook provides a more reliable solution. However, this approach can sometimes lead to redundant data storage, as data may be cached within the hook as well as within your models. mst-query offers a convenient method for fetching data directly within your components, seamlessly integrating with MST: ```tsx // Regular MST: const Todo = observer(({ id }) => { useEffect(() => { store.loadTodo(id); }, [id]); if (store.todoError) return
Got an error...
; if (store.todoIsLoading) return
Is loading...
; return ; }); // With mst-query: const Todo = observer(({ id }) => { const { data, error, isLoading } = useQuery(store.todoQuery, { request: { id } }) if (error) return
Got an error...
; if (isLoading) return
Is loading...
; return ; }); ``` ## Creating queries In mst-query, queries are treated as models. This means you can observe and update them just like regular models. You define a query model using `createQuery`: ```ts const LoadTodoQuery = createQuery("LoadTodoQuery", { data: t.reference(Todo), request: t.model({ id: t.string }), async endpoint({ request }) { return todoApi.get(request.id) } }); ``` The first option, `data`, represents the shape of the data returned from the endpoint. The second option, `request`, represents the arguments passed to the endpoint function. Both data and request undergo runtime type checking. ## Automatic normalization A unique feature of mst-query is that data received from the server is automatically normalized. Because queries already understand the shape of the data returned from the API they consume, we can automate the process of creating and updating models with identifiers: ```ts import { t, flow } from "mobx-state-tree" const User = t.model("User", { id: t.identifier, name: t.string }) const Todo = t.model("Todo", { id: t.identifier, title: t.string, message: t.string, done: t.boolean, createdBy: t.reference(User) }) // Regular MST: const TodoStore = t .model("RootStore", { todos: t.map(Todo) }) .actions((self) => ({ loadTodo: flow(function* loadTodo(todoId: string) { const todo = yield todoApi.getTodo(todoId); const root = getRoot(self); const user = root.userStore.createOrUpdateUser(todo.createdBy); todo.createdBy = user; const oldTodo = self.todos.get(todoId); if (!oldTodo) { self.todos.put({ todo }); } else { self.todos.put({ ...getSnapshot(oldTodo), ...todo }); } }) })) // With mst-query: const UserStore = createModelStore('UserStore', User); const TodoStore = createModelStore("TodoStore", Todo).props({ todoQuery: createQuery("TodoQuery", { data: t.reference(Todo), request: t.model({ id: t.string }), async endpoint({ request }) { return todoApi.getTodo(request.id) } }) }) const RootStore = createRootStore({ userStore: t.optional(UserStore, {}), todoStore: t.optional(TodoStore, {}) }); ``` The functions `createRootStore` and `createModelStore` let mst-query know about your models that should be normalized. Note that you don't have to manually update the createdBy property on the todo, as this is done automatically for you. In this example, we only had one nested data model in our response. However, in a real-world scenario, such as querying a GraphQL endpoint, you may need to handle dozens of similar properties. Mst-query will normalize all of these for you without additional code. ## Query invalidation upon stale data Just like in react-query, you can pass a `staleTime` option to `useQuery`. This ensures your data gets periodically updated as the user navigates through your app. The default value of `staleTime` is 0, which means your users always see fresh data. In mst-query, models are also automatically updated when you use `createMutation` and `mutate`. The only requirements are that your API returns the new data and that the data property is a reference type: ```ts const TodoRequestModel = t.model({ id: t.string, done: t.boolean, title: t.string }); const TodoUpdateMutation = createMutation("TodoUpdateMutation", { data: t.reference(Todo), request: TodoRequestModel, async endpoint({ request }) { return todoApi.update(request) } }); const TodoStore = createModelStore("TodoStore", Todo) .props({ todoQuery: TodoQuery, todoUpdateMutation: TodoUpdateMutation }) .actions(self => ({ update(data) { // When mutate successfully resolves, the Todo will be automatically updated. self.todoUpdateMutation.mutate({ request: data }); } })) ``` You can also manually refetch a query by calling `invalidate`. This pairs nicely with `createMutation` and a new listener called `onMutate`. A common use case for this is refetching a list of items: ```tsx const TodoListQuery = createQuery("TodoListQuery", { data: t.array(t.reference(Todo)), async endpoint() { return todoApi.getList(); } }); const TodoAddMutation = createMutation("TodoAddMutation", { data: t.reference(Todo), request: TodoRequestModel, async endpoint({ request }) { return todoApi.update(request) } }); const TodoStore = createModelStore("TodoStore", Todo) .props({ todoListQuery: TodoListQuery, todoAddMutation: todoAddMutation }) .actions(self => ({ afterCreate() { onMutate(self.todoAdd, (result) => { // Call invalidate to refetch the list... self.todoListQuery.invalidate(); // ...or add the new item directly if you don't need to refetch self.todoListQuery.data.push(result); }); } })); const TodoListContainer = observer(() => { const { data } = useQuery(store.todoListQuery); const [addTodo, { isLoading }] = useMutation(store.todoAddMutation); return ; }); ``` ## Imperative api Using hooks is convenient, but sometimes your data fetching logic can become more complex, resulting in a lot of business logic in your components. Thankfully, most things you can do with hooks can also be accomplished with an imperative API: ```tsx const TodoStore = createModelStore("TodoStore", Todo) .props({ todoQuery: TodoQuery, todoUpdateMutation: TodoUpdateMutation }) .volatile(self => ({ permssionError: '', updateResult: null })) .actions(self => ({ updateTodo: flow(function* (request) { const result = yield todoApi.checkPermissions(request.id); if (!result.ok) { self.permissionError = 'You are not allowed to edit this resource'; return; } const { error, result: updateResult } = yield self.todoUpdateMutation.mutate({ request }); if (error) { logApi.sendLog(error.message); } self.updateResult = updateResult; }); })); const TodoLoader = async (id) => { // Manual fetch in a route loader. This is also how you prefetch data. const todo = await store.todoQuery.query({ request: { id } }); return ; }; const TodoContainer = observer((props) => { const { todo, store } = props; return ( ); }); ``` The imperative API supports most of the features in mst-query. However, automatically refetching a query when it's stale—either by passing `staleTime` or calling `invalidate`—is currently not supported. ## Optimistic update Optimistic updates are important for a UI to feel responsive. You achieve this in mst-query by passing your update to the `optimisticUpdate` option in `mutate`. When the mutate call resolves, whether successfully or not, the optimistic update is automatically rolled back. ```ts const serverTodo = yield self.todoAddMutation.mutate({ request: data, optimisticUpdate() { // createModelStore provides a merge action that you can use to manually create models const clientTodo = todoStore.merge({ id: `${Math.random()}`, title: data.title, done: data.done, createdBy: loggedInUserId }); todoStore.todoListQuery.push(clientTodo); } }); todoStore.todoListQuery.push(serverTodo); ``` ## Garbage collection Consider a scenario where an MST application fetches a list of items from an API. Over time, items may be added, updated, or removed. In regular MST, every item fetched remains in memory unless you manually remove them. If the list is paginated, the problem is even larger. Since mst-query tracks all models via queries, it can safely remove unused models. You do this by calling runGc on the rootStore: `rootStore.runGc()` ================================================ FILE: docs/recipes/pre-built-form-types-with-mst-form-type.md ================================================ --- id: pre-built-form-types-with-mst-form-type title: Pre-built Form Types with MST Form Type --- Find the `mst-form-type` library on npm: https://www.npmjs.com/package/mst-form-type. ## Introduction Libraries like [Ant Design](https://ant.design/) have a built-in `Form` component that holds field status and validation rules. Integrating a `Form` component with MobX State Tree models can pose significant challenges as business logic become more complex. That's where a solution like `mst-form-type` library comes into play. It models the Ant design field management like a conventional MobX-State-Tree type definition. Users can still use the UI of a component library and keep logic inside MobX-State-Tree, instead of syncing status changes between `Form` component and model. Please note: The `mst-form-type` library primarily provides model types for the form structure. It does not encompass business logic related to field interactions. ## Setup Setting up and utilizing the `mst-form-type` library is straightforward: 1. Install the `mst-form-type` library via npm: ```sh npm install mst-form-type ``` 2. Ensure you have `mobx-state-tree ^5.0.0` installed: ```sh npm install mobx-state-tree@^5.0.0 ``` Now that the general installation is complete, let's explore how to use it through a simple example. Although mobx-state-tree is designed to handle complex web application states, you might find the example a bit over-designed. However, the aim is to illustrate the idea of why using mst-form-type is helpful, so I've kept the logic as simple as possible. ## Example To demonstrate the difference between mst-form-type and the Form component, I've created a comparative example. The form is straightforward, featuring two static fields and a dynamic field group with two fields inside. Certain fields have validation rules to illustrate how validation functions. Additionally, the value of a static field changes when the number of dynamic fields reaches a certain threshold. ### Ant Design `Form` component I've utilized the [Ant Design](https://ant.design/) `Form` component for the example, although other libraries should have very similar implementations. Let's take a look at the Ant Design version first. ```tsx import React, { useState, useEffect } from 'react' import { Form, Input, Select, Button, InputNumber } from 'antd' const { Option } = Select const MyForm = () => { const [form] = Form.useForm() // extra state to handle field interaction logic const [members, setMembers] = useState([{ name: '', age: '' }]) const [planValue, setPlanValue] = useState('') useEffect(() => { // handle field interaction in effect if (members.length > 1 && planValue === 'A') { form.setFieldsValue({ plan: 'B' }) } if (members.length > 3 && planValue !== 'C') { form.setFieldsValue({ plan: 'C' }) } }, [members, planValue, form]) const onFinish = values => { console.log('Received values:', values) } // handle field interaction by another set of onChange events const handleAddMember = cb => { if (members.length <= 5) { setMembers([...members, { name: '', age: '' }]) cb() } } const handleRemoveMember = (index, cb) => { const updatedMembers = [...members] updatedMembers.splice(index, 1) setMembers(updatedMembers) cb(index) } const handlePlanChange = value => { setPlanValue(value) } return ( {/* useForm hook provides a form instance to hold field status */}
{/* field need name props for form instance */} {/* extra event handler for field logic */} {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...restField }, index) => (
{/* extra event handler for field logic */}
))} )}
) } export default MyForm ``` The code reveals that to manage field logic, we need to maintain a second copy of the field value in React state and attach additional event handlers to Input or other components. Subsequently, we handle the logic within useEffect hooks using form instance APIs. However, this approach can quickly become unmaintainable as the logic complexity increases. Furthermore, it is challenging to integrate with other application states in MST. This is because MST does not inherently manage field statuses; instead, the form instance does so. ### mst-form-type Now, let's examine how the same example looks when using mst-form-type. The code can be split into two files: one for the UI and the other for the model. Let's start by reviewing the model file. ```typescript // model.ts import { types } from "mobx-state-tree" import createForm from "mst-form-type" // define form in schema export const FormSchema = { static: [ { id: "name", default: "", validator: "required" }, { id: "plan", default: "A" } ], dynamic: [ { id: "member", limit: 5, schema: [ { id: "name", default: "", validator: "required" }, { id: "age", default: "", validator: "required" } ], default: [{ name: "John", age: 20 }], onAdd: (i) => { // hooks run when add dynamic fields console.log("add", i) }, onRemove: (i) => { // hooks run when remove dynamic fields console.log("remove", i) }, onEdit: (i) => { // hooks run when edit dynamic fields, only be called when edit field by form action console.log("edit", i) } } ] } // App model export const Example = types .model("FormExample") .props({ form: createForm(FormSchema) // form as a model type }) .views((self) => ({ get disableA() { return self.form.member.size > 1 }, get disableB() { return self.form.member.size > 3 } })) .actions((self) => ({ onAddFields() { // field logic if (self.form.member.size > 1 && self.form.plan.value === "A") { self.form.plan.setValue("B") } else if (self.form.member.size > 3 && self.form.plan.value !== "C") { self.form.plan.setValue("C") } } })) ``` In the model file, we declare a form schema and create the form model. Subsequently, all relevant field models are created within the form model. An action is added to the model to handle field logic, illustrating that mst-form-type doesn't manage business logic but solely holds form and field statuses. Now, let's examine how the model is utilized by the UI, which is still built using Ant Design. ```tsx // model.ts import React, { useContext } from 'react' import { Form, Input, Select, Button, InputNumber } from 'antd' import { model, ModelContext } from '~/models' import { observer } from 'mobx-react-lite' const { Option } = Select const MyForm = () => { // get model instance via context const model = useContext(ModelContext) const onFinish = values => { // 在这里计算价格 console.log('Received values:', model.form.submit()) } return ( {/* no form instance need */}
{/* get field status from MST */} {/* controlled component using MST props and action */} model.form.name.setValue(e.target.value)} /> <> {/* render dynamic fields */} {model.form.member.fields.map(({ id, ...field }) => { return (
{/* access individual dynamic fields */} { field.name.setValue(e.target.value) }} /> field.age.setValue(e.target.value)} />
) })}
) } // wrap the component by mobx observer export default observer(MyForm) ``` Essentially, `mst-form-type` eliminates the need for a form instance, allowing us to have only one copy of field status, which resides within the MST. Components no longer need to manage form-related states; instead, they simply read state from the MST and render accordingly. There are two methods to retrieve or set field values. One involves using actions on each field instance, as demonstrated in the example code. The other method utilizes actions on the form type instance, specifying the field id defined in the form schema. Refer to the "APIs" section below for more details. ## How It Works The `mst-form-type` library exports a function which creates a form model (based on the base form model under the hood) using the provided schema and an optional name. Each form type model create its own type, and then instance by MST, which makes every attribute and method independent. So that you can have multiple form type `props` in a single MST model. User can conveniently interact with the form field via form props, or apply the form model actions to directly get or set field values. Let's see the architecture of `mst-form-type` first, and then we will go through most of the useful APIs ### Architecture The library defines three model types under the hood: - **Field Model**: Contains props and actions of a form field, such as `value`, `default`, `id`, `valid()`, etc. - **Group Model**: Built for handling a collection of field models as dynamic fields. Each dynamic field group should maintain uniformity in structure. The model also provides actions as life cycle hooks for adding, editing, and removing dynamic fields. - **Base Form Model**: Encapsulates all field and group models and the associated form methods. The exported function will build a form model with fields defined in schema on top of the base form model. ### APIs **default export** The default exported method will generate a new custom types.model with all the fields in the schema as props, based on a base model type. The newly created form type will automatically initialize with the schema upon creation. Optionally, a name can be passed for tracking purposes; otherwise, it will default to the base model name. ```typescript type TValidator = "required" | ((...args: any[]) => boolean) | RegExp | undefined | null type TValue = string | boolean | number | Record | Array interface FieldSchema { id: string type?: "string" | "number" | "boolean" | "object" | "array" default: TValue validator?: TValidator msg?: string } ``` #### Field ##### schema ```typescript type TValidator = "required" | ((...args: any[]) => boolean) | RegExp | undefined | null type TValue = string | boolean | number | Record | Array interface FieldSchema { id: string type?: "string" | "number" | "boolean" | "object" | "array" default: TValue validator?: TValidator msg?: string } ``` ##### props `id` `types.identifier`. The key of each field in a form, and will become the form type props key. It should be unique and will be used to access field value and in `setValue()` form action. `value` The props hold the field value. The value type can be `string`, `boolean`, `number`, `object`, `array`. `object` and `array` will be tranform to `types.frozen` as a MST leaf. `default` The default value of a field. The Mobx State Tree will decide `prop` type based on the type of this value. `validator` Optional & Private. All validators will be called in `valid()` before `submit()`. `'required'` means this field cannot be falsy values, like `0`, `''`, or `undefined`. `((...args: any[]) => boolean)` means a function return a boolean value. If returned `true`, the validation will be treated passed. `RegExp` means the value will be used in `RegExp.test()`. If returned `true`, the validation will be treated passed. `undefined | null` will not be processed. `msg` Optional. Message shows when field is invalid. The default message is `'The input is invalid'` `invalid` Compute value. Return revert value of `invalid()` result ##### actions **`setValue(value)`** Update field value. **`valid()`** Run field validator if it has. **`reset()`** Reset field value to default value. `setValidator(rawValidator: TValidator)` Change field validator after initialization `init(field: IField)` Rerun field initialization `setErrorMsg(msg: string)` Change invalid message `clear()` Set field value to `null` ##### code example ```typescript form[id].value form[id].invalid form[id].setValue("new-value") form[id].reset() form[id].valid() ``` #### Dynamic Field Group ##### schema ```typescript interface DynamicFields { id: string limit: number schema: FieldSchema | FieldSchema[] default?: Array> onAdd?: (field) => any onRemove?: (field) => any onEdit?: (field) => void } ``` ##### props `id` `types.identifier`. The key of each dynamic field group in a form, and will become the form type props key. It should be unique and will be used to access field value and in `setDynamicValue()` form action. `fields` An array holds all dynamic field models. `schema` in the interface is to define field schema here. Object in `default` array will be used to create dynamic fields when form initializing. `limit` Optional. Maxium dynamic field allowed. default is `-1`, means unlimited. ##### actions **`addFields(i, isInit = false)`** Add new dynamic field `i`. You don't need to pass isInit flag when calling the action. It is used for not calling `onAdd` hooks in schema when initialization. **`removeFields(id: string)`** Remove the field with specific `id`. This action will call the `onRemove` hook if passed. **`editField(id: string, fieldKey: string, value)`** Edit `fieldKey` field with `id` to `value`. This action will call the `onEdit` hook if passed. `getValues()` Get all dynamic field values/ `valid()` Valid all dynamic field. `reset()` Reset all dynamic fields. ##### code example ```typescript form[id].fields.map(field => { ... }) form[id].addFields(field) form[id].removeFields('id') form[id].editField('id', 'key', 'value') form[id].getValues() // get all dynamic field values, rarely used form[id].valid() // valid all dynamic field, rarely used form[id].reset() // reset all dynamic field, rarely used ``` #### Form ##### schema ```typescript interface FormSchema { static: FieldSchema[] dynamic?: DynamicFields[] } ``` ##### props **fields** Every field in `schema` will become a field `prop` of form type. The type of each field will be based on `default` value. **`submission`** A snapshot of last success submitted form values, in Key-Value object format. `error` An object in Key-Value format contains validation error of each field. This will be cleared on every `valid()` call. `_internalStatus` Indicate the form status, has 4 values: `'init', 'pending', 'success', 'error'`. Usually you don't need it, it will change according to form status. `loading` Compute value. Return `true` when form status is 'pending'. Designed for avoiding duplicated submission. ##### actions **`setValue({ key, value })`** Set **static** field value in a form type. `_internalStatus` is reserved. **`setDynamicValue({ groupId, id, key, value })`** Set **dynamic** field value in a form type. Each dynamic field has a field group id and a field id. Or you can use the `setValue` action on instance to do the same job. **`submit()`** It will return all field values if passed validation in Key-Value format object. The last valid submission will be hold in `submission` props. **The method will not submit the form in any form of action. It only output the form current values. You need to handle to real submission action yourself.** `init()` It will be called after new custom type created with schema. It will process the schema to get default values and validators. `valid()` It will run all validators in schema with current field values. It will be called in `submit()`, and produce `error` if any error happens. `reset()` It will set the form type to `init` status, clearing submissions and errors. All fields will be set to default values passed by schema. ##### code example ```typescript form.setValue({ key, value }) form.setDynamicValue({ groupId, id, key, value }) form.submit() form.rest() form.onAdd(id, field) form.onRemove(groupId, fieldId) form.onEdit(groupId, fieldId, key, value) form.clear(groupId) ``` ================================================ FILE: docs/tips/circular-deps.md ================================================ --- id: circular-deps sidebar_label: Circular dependencies title: Handle circular dependencies between files and types using `late` ---
In the exporting file: ```javascript export function LateStore() { return types.model({ title: types.string }) } ``` In the importing file: ```javascript import { LateStore } from "./circular-dep" const Store = types.late(() => LateStore) ``` Thanks to function hoisting in combination with `types.late`, this lets you have circular dependencies between types, across files. If you are using TypeScript and you get errors about circular or self-referencing types then you can partially fix it by doing: ```ts const Node = types.model({ x: 5, // as an example me: types.maybe(types.late((): IAnyModelType => Node)) }) ``` In this case, while "me" will become any, any other properties (such as x) will be strongly typed, so you can typecast the self referencing properties (me in this case) once more to get typings. For example: ```ts node.((me) as Instance).x // x here will be number ``` ================================================ FILE: docs/tips/faq.md ================================================ --- id: faq title: Frequently Asked Questions ---
### Should all state of my app be stored in `mobx-state-tree`? No, or not necessarily. An application can use both state trees and vanilla MobX observables at the same time. State trees are primarily designed to store your domain data as this kind of state is often distributed and not very local. For local component state, for example, vanilla MobX observables might often be simpler to use. ### When not to use MST? MST provides access to snapshots, patches and interceptable actions. Also, it fixes the `this` problem. All these features have a downside as they incur a little runtime overhead. Although in many places the MST core can still be optimized significantly, there will always be a constant overhead. If you have a performance critical application that handles a huge amount of mutable data, you will probably be better off by using 'raw' MobX, which has a predictable and well-known performance and much less overhead. Likewise, if your application mainly processes stateless information (such as a logging system), MST won't add much value. ### Can I use Hot Module Reloading?
egghead.io lesson 10: Restore the Model Tree State using Hot Module Reloading when Model Definitions Change
Hosted on egghead.io
Yes, with MST it is pretty straight forward to setup hot reloading for your store definitions while preserving state. See the [todomvc example](https://github.com/coolsoftwaretyler/mst-example-todomvc/blob/main/src/index.js#L59C1-L64C1). ### How does MST compare to Redux So far this might look a lot like an immutable state tree as found for example in Redux apps, but there're are only so many reasons to use Redux as per [article linked at the very top of Redux guide](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367) that MST covers too, meanwhile: - Like Redux, and unlike MobX, MST prescribes a very specific state architecture. - mobx-state-tree allows direct modification of any value in the tree. It is not necessary to construct a new tree in your actions. - mobx-state-tree allows for fine-grained and efficient observation of any point in the state tree. - mobx-state-tree generates JSON patches for any modification that is made. - mobx-state-tree provides utilities to turn any MST tree into a valid Redux store. - Having multiple MSTs in a single application is perfectly fine. ### Where is the `any` type? MST doesn't offer an any type because it can't reason about it. For example, given a snapshot and a field with `any`, how should MST know how to deserialize it or apply patches to it, etc.? If you need `any`, there are following options: 1. Use `types.frozen()`. Frozen values need to be immutable and serializable (so MST can treat them verbatim) 2. Use volatile state. Volatile state can store anything, but won't appear in snapshots, patches etc. 3. If your type is regular, and you just are too lazy to type the model, you could also consider generating the type at runtime once (after all, MST types are just JS...). However, you will lose static typing, and any confusion it causes is up to you to handle :-). ================================================ FILE: docs/tips/inheritance.md ================================================ --- id: inheritance sidebar_label: Simulating inheritance title: Simulate inheritance by using type composition ---
There is no notion of inheritance in MST. The recommended approach is to keep references to the original configuration of a model in order to compose it into a new one, for example by using `types.compose` (which combines two types) or producing fresh types using `.props|.views|.actions`. An example of classical inheritance could be expressed using composition as follows: ```javascript const Square = types .model( "Square", { width: types.number } ) .views(self => ({ // note: this is not a getter! this is just a function that is evaluated surface() { return self.width * self.width } })) // create a new type, based on Square const Box = Square .named("Box") .views(self => { // save the base implementation of surface, again, this is a function. // if it was a getter, the getter would be evaluated only once here // instead of being able to evaluate dynamically at time-of-use const superSurface = self.surface return { // super contrived override example! surface() { return superSurface() * 1 }, volume() { return self.surface() * self.width } } })) // no inheritance, but, union types and code reuse const Shape = types.union(Box, Square) const instance = Shape.create({type:"Box",width:4}) console.log(instance.width) console.log(instance.surface()) // calls Box.surface() console.log(instance.volume()) // calls Box.volume() ``` Similarly, compose can be used to simply mix in types: ```javascript const CreationLogger = types.model().actions((self) => ({ afterCreate() { console.log("Instantiated " + getType(self).name) } })) const BaseSquare = types .model({ width: types.number }) .views((self) => ({ surface() { return self.width * self.width } })) export const LoggingSquare = types .compose( // combine a simple square model... BaseSquare, // ... with the logger type CreationLogger ) // ..and give it a nice name .named("LoggingSquare") ``` ================================================ FILE: docs/tips/more-tips.md ================================================ --- id: more-tips title: Miscellaneous Tips ---
### Generate MST models from JSON The following service can generate MST models based on JSON: https://transform.now.sh/json-to-mobx-state-tree ### `optionals` and default value functions `types.optional` can take an optional function parameter which will be invoked each time a default value is needed. This is useful to generate timestamps, identifiers or even complex objects, for example: `createdDate: types.optional(types.Date, () => new Date())` ### `toJSON()` for debugging For debugging you might want to use `getSnapshot(model, applyPostProcess)` to print the state of a model. If you didn't import `getSnapshot` while debugging in some debugger, don't worry, `model.toJSON()` will produce the same snapshot. (For API consistency, this feature is not part of the typed API). #### Optional/empty maps/arrays Since v3, maps and arrays are optional by default, this is: ```javascript types.map(OtherType) // is the same as types.optional(types.map(OtherType), {}) types.array(OtherType) // is the same as types.optional(types.array(OtherType), []) ``` #### Complex union types Union types works well when the types are primitives. Unions might cause bugs when complex types are involved. For Example ```javascript const Foo = types.model({ foo: types.array(types.string) }) const Bar = types.model({ bar: types.array(types.number) }) const FooBar = types.union(Foo, Bar) const test_foo = { foo: ["test"] } const test_bar = { bar: [200] } const unionStore = Store.create({ foobars: [test_foo, test_bar] }) const foo = unionStore.foobars[0] const bar = unionStore.foobars[1] console.log(foo, bar) // Expected: { foo: ["test"], bar: [200] } // Actual: { foo: ["test"], foo: [] } ``` This can be solved in two ways **Using Dispatcher:** You can provide first arg with a _dispatcher_ that provides _snapshot_ which can be used to explicitly return the `type` of the model ```javascript const FooBar = types.union( { dispatcher: (snapshot) => { console.log({ snapshot }) if (snapshot.foo) { return Foo } return Bar } }, Foo, Bar ) ``` **Using Literals:** Adding type literal to the Base Models to identify the type ```javascript const Foo = types.model({ foo: types.array(types.string), type: types.literal("foo") }) const Bar = types.model({ bar: types.array(types.number), type: types.literal("bar") }) ``` ### Building with production environment MobX-state-tree provides a lot of dev-only checks. They check the correctness of function calls and perform runtime type-checks over your models. It is recommended to disable them in production builds. To do so, you should use webpack's DefinePlugin to set environment as production and remove them. More information could be found in the [official webpack guides](https://webpack.js.org/plugins/environment-plugin/#usage). ================================================ FILE: docs/tips/resources.md ================================================ --- id: resources title: Talks & Blogs ---
## Official resources - Introduction blog post [The curious case of MobX state tree](https://medium.com/@mweststrate/the-curious-case-of-mobx-state-tree-7b4e22d461f) - Free [egghead.io](https://egghead.io/courses/manage-application-state-with-mobx-state-tree) MST course - [Introduction tutorial](../intro/getting-started) ## Talks - Talk React Europe 2017: [Next generation state management](https://www.youtube.com/watch?v=rwqwwn_46kA) - Talk ReactNext 2017: [React, but for Data](https://www.youtube.com/watch?v=xfC_xEA8Z1M&index=6&list=PLMYVq3z1QxSqq6D7jxVdqttOX7H_Brq8Z) ([slides](http://react-next-2017-slides.surge.sh/#1), [demo](https://codesandbox.io/s/8y4p23j32j)) - Talk ReactJSDay Verona 2017: [Immutable or immutable? Both!](https://www.youtube.com/watch?v=zdtwaa5Rmb8&index=9&list=PLWK9j6ps_unl293VhhN4RYMCISxye3xH9) ([slides](https://mweststrate.github.io/reactjsday2017-presentation/index.html#1), [demo](https://github.com/mweststrate/reatjsday2017-demo)) - Talk React Alicante 2017: [Mutable or Immutable? Let's do both!](https://www.youtube.com/watch?v=DgnL3uij9ec&list=PLd7nkr8mN0sWvBH_s0foCE6eZTX8BmLUM&index=9) ([slides](https://mattiamanzati.github.io/slides-react-alicante-2017/#2)) - Talk ReactiveConf 2016: [Immer-mutable state management](https://www.youtube.com/watch?v=Ql8KUUUOHNc&list=PLa2ZZ09WYepMCRRGCRPhTYuTCat4TiDlX&index=30) - Talk FrontendLove 2018: [MobX State Tree + React: pure reactivity served](https://www.youtube.com/watch?v=HS9revHrNRI) by [Luca Mezzalira](https://lucamezzalira.com/) ([slides](https://docs.google.com/presentation/d/1f18RhN9hz1GPAdY4binWVNZDKm3k7EfNvV48lWnzdjQ/edit#slide=id.g35f391192_00)). - Talk React Native EU 2019: [Resolving the Great State Debate with Hooks, Context, and MobX-State-Tree](https://www.youtube.com/watch?v=Wx9slbOTD6Q) by [Jamon Holmgren](https://jamonholmgren.com/) ([slides](https://infinite-red.slides.com/infinitered/resolving-the-great-state-debate)) - International JavaScript Conference 2019, Londen [You don’t know MobX State Tree](https://www.youtube.com/watch?v=LKyCJB27oNM), by [Max Gallo](https://twitter.com/_maxgallo) - React Europe 2019: [Combining GraphQL + mobx-state-tree](https://www.youtube.com/watch?v=Sq2M00vghqY) ## Blogs / Vlogs - [Introduction to MobX State Tree](https://www.youtube.com/watch?v=pPgOrecfcg4) by [Leigh Halliday](https://twitter.com/leighchalliday) - [Creating a Trivia App with Ignite Bowser](https://shift.infinite.red/creating-a-trivia-app-with-ignite-bowser-part-1-1987cc6e93a1) by [Robin Heinze](https://twitter.com/robin_heinze) - [The MobX book](https://books.google.nl/books?id=ALFmDwAAQBAJ&pg=PP1&lpg=PP1&dq=michel+weststrate+mobx+quick+start+guide:+supercharge+the+client+state+in+your+react+apps+with+mobx&source=bl&ots=D460fxti0F&sig=ivDGTxsPNwlOjLHrpKF1nweZFl8&hl=nl&sa=X&ved=2ahUKEwiwl8XO--ncAhWPmbQKHWOYBqIQ6AEwAnoECAkQAQ#v=onepage&q=michel%20weststrate%20mobx%20quick%20start%20guide%3A%20supercharge%20the%20client%20state%20in%20your%20react%20apps%20with%20mobx&f=false) by Pavan Podila and Michel Weststrate, discusses MobX-State-Tree as well. - [How to: mobx-state-tree + react + typescript](https://dev.to/margaretkrutikova/how-to-mobx-state-tree-react-typescript-3d5j) by [Margarita Krutikova](https://twitter.com/rita_krutikova) - [MobX-state-tree: A step by step guide for React Apps](https://medium.com/mr-frontend-community/mobx-state-tree-a-step-by-step-guide-for-react-apps-e65716a219d2) by [Faris Tangastani](https://medium.com/@ftangastani) ## Supported devtools: - [Reactotron](https://github.com/infinitered/reactotron) - [MobX DevTools](https://chrome.google.com/webstore/detail/mobx-developer-tools/pfgnfdagidkfgccljigdamigbcnndkod) - The Redux can be connected as well as demonstrated [here](https://github.com/coolsoftwaretyler/mst-example-redux-todomvc/blob/main/src/index.js#L6) ## Addons, libraries and tools - [mst-query](https://github.com/ConrabOpto/mst-query/) - Query library for MST with support for auto normalization, garbage collection, infinite scroll & pagination, and more ================================================ FILE: docs/tips/snapshots-as-values.md ================================================ --- id: snapshots-as-values title: Using snapshots as values ---
Everywhere where you can modify your state tree and assign a model instance, you can also just assign a snapshot, and MST will convert it to a model instance for you. However, that is simply not expressible in static type systems atm (as the type written to a value differs to the type read from it). As a workaround MST offers a `cast` function, which will try to fool the typesystem into thinking that an snapshot type (and instance as well) is of the related instance type. ```typescript const Task = types.model({ done: false }) const Store = types.model({ tasks: types.array(Task), selection: types.maybe(Task) }) const s = Store.create({ tasks: [] }) // `{}` is a valid snapshot of Task, and hence a valid task, MST allows this, but TS doesn't, so we need to use 'cast' s.tasks.push(cast({})) s.selection = cast({}) ``` Additionally, for function parameters, MST offers a `SnapshotOrInstance` type, where T can either be a `typeof TYPE` or a `typeof VARIABLE`. In both cases it will resolve to the union of the input (creation) snapshot and instance type of that TYPE or VARIABLE. Using both at the same time we can express property assignation of complex properties in this form: ```typescript const Task = types.model({ done: false }) const Store = types .model({ tasks: types.array(Task) }) .actions(self => ({ addTask(task: SnapshotOrInstance) { self.tasks.push(cast(task)) }, replaceTasks(tasks: SnapshotOrInstance) { self.tasks = cast(tasks) } })) const s = Store.create({ tasks: [] }) s.addTask({}) // or s.addTask(Task.create({})) s.replaceTasks([{ done: true }]) // or s.replaceTasks(types.array(Task).create([{ done: true }])) ``` Additionally, the `castToSnapshot` function can be also used in the inverse case, this is when you want to use an instance inside an snapshot. In this case MST will internally convert the instance to a snapshot before using it, but we need once more to fool TypeScript into thinking that this instance is actually a snapshot. ```typescript const task = Task.create({ done: true }) const Store = types.model({ tasks: types.array(Task) }) // we cast the task instance to a snapshot so it can be used as part of another snapshot without typing errors const s = Store.create({ tasks: [castToSnapshot(task)] }) ``` Finally, the `castToReferenceSnapshot` can be used when we want to use an instance to actually use a reference snapshot (a string or number). In this case MST will internally convert the instance to a reference snapshot before using it, but we need once more to fool TypeScript into thinking that this instance is actually a snapshot of a reference. ```typescript const task = Task.create({ id: types.identifier, done: true }) const Store = types.model({ tasks: types.array(types.reference(Task)) }) // we cast the task instance to a reference snapshot so it can be used as part of another snapshot without typing errors const s = Store.create({ tasks: [castToReferenceSnapshot(task)] }) ``` ================================================ FILE: docs/tips/typescript.md ================================================ --- id: typescript title: TypeScript and MST ---
TypeScript support is best-effort as not all patterns can be expressed in TypeScript. Except for assigning snapshots to properties we get pretty close! As MST uses the latest fancy TypeScript features it is required to use TypeScript 5.3.3 or later with `noImplicitThis` and `strictNullChecks` enabled. The more strict options that are enabled, the better the type system will behave. #### Recommend compiler flags The recommended compiler flags (against which all our tests are written) are: ```json { "strictNullChecks": true, "strictFunctionTypes": true, "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true } ``` Or shorter by leveraging `strict`: ```json { "strict": true, "noImplicitReturns": true } ``` Flow is not supported. #### Using a MST type at design time When using models, you write an interface, along with its property types, that will be used to perform type checks at runtime. What about compile time? You can use TypeScript interfaces to perform those checks, but that would require writing again all the properties and their actions! Good news! You don't need to write it twice! There are four kinds of types available, plus one helper type: - `Instance` or `Instance` is the node instance type. (Legacy form is `typeof MODEL.Type`). - `SnapshotIn` or `SnapshotIn` is the input (creation) snapshot type. (Legacy form is `typeof MODEL.CreationType`). - `SnapshotOut` or `SnapshotOut` is the output (creation) snapshot type. (Legacy form is `typeof MODEL.SnapshotType`). - `SnapshotOrInstance` or `SnapshotOrInstance` is `SnapshotIn | Instance`. This type is useful when you want to declare an input parameter that is able consume both types. - `TypeOfValue` gets the original type for the given instance. Note that this only works for complex values (models, arrays, maps...) but not for simple values (number, string, boolean, string, undefined). ```typescript import { types, Instance, SnapshotIn, SnapshotOut } from "mobx-state-tree" const Todo = types .model({ title: "hello" }) .actions((self) => ({ setTitle(v: string) { self.title = v } })) interface ITodo extends Instance {} // => { title: string; setTitle: (v: string) => void } interface ITodoSnapshotIn extends SnapshotIn {} // => { title?: string } interface ITodoSnapshotOut extends SnapshotOut {} // => { title: string } ``` Note, it is important to use `interface` and not `type` when constructing those types! Although `type`s will work exactly the same, due to their nature they will be [much more expensive for the compiler to typecheck](https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections). For further performance tips, read the [official TypeScript performance wiki](https://github.com/microsoft/TypeScript/wiki/Performance). #### Typing `self` in actions and views The type of `self` is what `self` was **before the action or views blocks starts**, and only after that part finishes, the actions will be added to the type of `self`. Sometimes you'll need to take into account where your typings are available and where they aren't. The code below will not compile: TypeScript will complain that `self.upperProp` is not a known property. Computed properties are only available after `.views` is evaluated. For example: ```typescript const Example = types .model("Example", { prop: types.string }) .views((self) => ({ get upperProp(): string { return self.prop.toUpperCase() }, get twiceUpperProp(): string { return self.upperProp + self.upperProp // Compile error: `self.upperProp` is not yet defined } })) ``` You can circumvent this situation by using `this` whenever you intend to use the newly declared computed values that are local to the current object: ```typescript const Example = types.model("Example", { prop: types.string }).views((self) => ({ get upperProp(): string { return self.prop.toUpperCase() }, get twiceUpperProp(): string { return this.upperProp + this.upperProp } })) ``` Alternatively you can also declare multiple `.views` block, in which case the `self` parameter gets extended after each block. ```typescript const Example = types .model('Example', { prop: types.string }) .views(self => { get upperProp(): string { return self.prop.toUpperCase(); }, })) .views(self => ({ get twiceUpperProp(): string { return self.upperProp + self.upperProp; }, })); ``` As a last resort, although not recommended due to the performance penalty (see the note below), you may declare the views in two steps: ```typescript const Example = types .model('Example', { prop: types.string }) .views(self => { const views = { get upperProp(): string { return self.prop.toUpperCase(); }, get twiceUpperProp(): string { return views.upperProp + views.upperProp; } } return views })) ``` _**NOTE: the last approach will incur runtime performance penalty as accessing such computed values (e.g. inside `render()` method of an observed component) always leads to full recompute (see [this issue](https://github.com/mobxjs/mobx-state-tree/issues/818#issue-323164363) for details). For a heavily used computed properties it's recommended to use one of above approaches.**_ Similarly, when writing actions or views one can use helper functions: ```typescript import { types, flow } from "mobx-state-tree" const Example = types.model("Example", { prop: types.string }).actions((self) => { // Don't forget that async operations HAVE // to use `flow( ... )`. const fetchData = flow(function* fetchData() { yield doSomething() }) return { fetchData, afterCreate() { // Notice that we call the function directly // instead of using `self.fetchData()`. This is // because Typescript doesn't know yet about `fetchData()` // being part of `self` in this context. fetchData() } } }) ``` #### Using cast We provide a `cast` utility to [cast a node snapshot to an instance type for assignment](https://mobx-state-tree.js.org/API/#cast). [Check out this blog post for details and examples on when to use it](https://coolsoftware.dev/blog/type-casting-in-mobx-state-tree/). ================================================ FILE: jest.config.js ================================================ module.exports = { displayName: "test", testEnvironment: "node", transform: { "^.+\\.tsx?$": "ts-jest" }, testRegex: ".*\\.test\\.tsx?$", moduleFileExtensions: ["ts", "tsx", "js"], globals: { "ts-jest": { tsConfig: "__tests__/tsconfig.json" } }, reporters: ["default", "jest-junit"] } ================================================ FILE: package.json ================================================ { "name": "mobx-state-tree", "version": "7.0.2", "description": "Opinionated, transactional, MobX powered state container", "main": "dist/mobx-state-tree.js", "umd:main": "dist/mobx-state-tree.umd.js", "module": "dist/mobx-state-tree.module.js", "browser": { "./dist/mobx-state-tree.js": "./dist/mobx-state-tree.js", "./dist/mobx-state-tree.module.js": "./dist/mobx-state-tree.module.js" }, "unpkg": "dist/mobx-state-tree.umd.min.js", "jsnext:main": "dist/mobx-state-tree.module.js", "react-native": "dist/mobx-state-tree.module.js", "typings": "dist/index.d.ts", "sideEffects": false, "scripts": { "clean": "shx rm -rf dist lib", "build": "bun run clean && tsc && cpr lib/src dist --filter=\\.js$ && rollup -c", "test:dev": "NODE_ENV=development bun test", "test:prod": "MST_TESTING=1 NODE_ENV=production bun test", "test:all": "bun run test:dev && bun run test:prod && bun run size", "test:perf": "bun test ./__tests__/perf/", "test:perf:compiled": "bun run build && bun run jest:perf && TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' /usr/bin/time node --expose-gc --require ts-node/register __tests__/perf/report.ts", "typecheck": "tsc --noEmit", "size": "size-limit", "tag-new-version": "yarn version && git push --tags", "build-docs": "bun run fix-typedoc && shx rm -rf ./docs/API && typedoc --options ./typedocconfig.js && node scripts/fix-docs-source-links.js", "publish-docs": "bun run build-docs && cd ./website && GIT_USER=jamonholmgren USE_SSH=true bun run publish-gh-pages", "start": "cd website && bun run start", "prepare": "husky install", "lint": "tslint -c ./tslint.json 'src/**/*.ts'", "fix-typedoc": "shx rm -rf ./node_modules/typedoc/node_modules/typescript", "prettier:list": "prettier --list-different \"src/**/*.ts\" \"__tests__/**/*.ts\"", "prettier:check": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\"", "prettier:write": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ "prettier --write" ] }, "repository": { "type": "git", "url": "https://github.com/mobxjs/mobx-state-tree.git" }, "author": "Michel Weststrate", "license": "MIT", "bugs": { "url": "https://github.com/mobxjs/mobx-state-tree/issues" }, "files": [ "dist/" ], "dependencies": { "ts-essentials": "^9.4.1" }, "devDependencies": { "@size-limit/preset-big-lib": "^5.0.3", "@types/bun": "^1.0.6", "@types/node": "^12.0.2", "concat": "^1.0.3", "coveralls": "^3.1.0", "cpr": "^3.0.1", "husky": "^7.0.0", "lint-staged": "^11.1.2", "mobx": "^6.13.1", "prettier": "3.4.1", "rollup": "^2.18.1", "rollup-plugin-commonjs": "^10.0.0", "rollup-plugin-filesize": "^9.0.1", "rollup-plugin-node-resolve": "^5.0.0", "rollup-plugin-replace": "^2.1.0", "rollup-plugin-terser": "^6.1.0", "shx": "^0.3.2", "size-limit": "^5.0.3", "ts-node": "^8.10.2", "tslib": "^2.0.0", "tslint": "^6.1.3", "tslint-config-prettier": "^1.18.0", "typedoc": "0.15.8", "typedoc-plugin-markdown": "2.2.11", "typescript": "^5.3.3", "yarn-deduplicate": "^3.1.0" }, "peerDependencies": { "mobx": "^6.3.0" }, "keywords": [ "mobx", "mobx-state-tree", "promise", "reactive", "frp", "functional-reactive-programming", "state management" ], "gitHead": "27ec7ac0b0743a367fb01a7f40192f3042bd91f2", "prettier": { "printWidth": 100, "semi": false, "tabWidth": 4, "singleQuote": false, "trailingComma": "none", "arrowParens": "avoid", "useTabs": false }, "size-limit": [ { "path": "./dist/mobx-state-tree.min.js", "limit": "25 KB", "webpack": false, "running": false } ] } ================================================ FILE: rollup.config.js ================================================ import * as path from "path" import filesize from "rollup-plugin-filesize" import resolve from "rollup-plugin-node-resolve" import { terser } from "rollup-plugin-terser" import replace from "rollup-plugin-replace" const devPlugins = () => [resolve(), filesize()] // For umd builds, set process.env.NODE_ENV to "development" since 'process' is not available in the browser const devPluginsUmd = () => [ resolve(), replace({ "process.env.NODE_ENV": JSON.stringify("development") }), filesize() ] const prodPlugins = () => [ resolve(), replace({ "process.env.NODE_ENV": JSON.stringify("production") }), terser(), filesize() ] const config = (outFile, format, mode) => ({ input: "./lib/src/index.js", output: { file: path.join("./dist", outFile), format: format, globals: { mobx: "mobx" }, name: format === "umd" ? "mobxStateTree" : undefined }, external: ["mobx"], plugins: mode === "production" ? prodPlugins() : format === "umd" ? devPluginsUmd() : devPlugins() }) export default [ config("mobx-state-tree.js", "cjs", "development"), config("mobx-state-tree.min.js", "cjs", "production"), config("mobx-state-tree.umd.js", "umd", "development"), config("mobx-state-tree.umd.min.js", "umd", "production"), config("mobx-state-tree.module.js", "es", "development") ] ================================================ FILE: scripts/fix-docs-source-links.js ================================================ /** * Rewrites GitHub source links in generated API docs: * 1. Point to the canonical mobxjs/mobx-state-tree repo (so forks produce main-repo links). * 2. Use the last commit hash that changed each linked file (from git), * so links stay valid and minimize the changes upon subsequent doc builds. */ const fs = require("fs") const path = require("path") const { execSync } = require("child_process") const DOCS_API = path.join(__dirname, "..", "docs", "API") const REPO_ROOT = path.join(__dirname, "..") const CANONICAL_BASE = "https://github.com/mobxjs/mobx-state-tree/blob" // Match "Defined in [path:line](https://github.com/owner/mobx-state-tree/blob/hash/path#Lline)" const LINK_REGEX = /https:\/\/github\.com\/[^/]+\/mobx-state-tree\/blob\/[a-f0-9]+\/([^#]+)#L(\d+)/g function getAllMdContent(dir) { let out = "" const entries = fs.readdirSync(dir, { withFileTypes: true }) for (const ent of entries) { const full = path.join(dir, ent.name) if (ent.isDirectory()) { out += getAllMdContent(full) } else if (ent.name.endsWith(".md")) { out += fs.readFileSync(full, "utf8") } } return out } function getUniqueFilePathsFromLinks(content) { const paths = new Set() let m while ((m = LINK_REGEX.exec(content)) !== null) { paths.add(m[1]) // file path (e.g. src/utils.ts) } return [...paths] } function buildFileToHashMap(filePaths) { const map = {} for (const filePath of filePaths) { try { const hash = execSync("git log -1 --format=%h -- " + JSON.stringify(filePath), { cwd: REPO_ROOT, encoding: "utf8" }).trim() if (hash) map[filePath] = hash } catch (_) { // file not in git or not found, skip } } return map } function fixFile(filePath, fileToHash) { let content = fs.readFileSync(filePath, "utf8") const fixed = content.replace(LINK_REGEX, (match, pathInRepo, line) => { const hash = fileToHash[pathInRepo] if (!hash) return match return `${CANONICAL_BASE}/${hash}/${pathInRepo}#L${line}` }) if (fixed !== content) { fs.writeFileSync(filePath, fixed, "utf8") } } function walk(dir, fileToHash) { const entries = fs.readdirSync(dir, { withFileTypes: true }) for (const ent of entries) { const full = path.join(dir, ent.name) if (ent.isDirectory()) { walk(full, fileToHash) } else if (ent.name.endsWith(".md")) { fixFile(full, fileToHash) } } } if (!fs.existsSync(DOCS_API)) { console.warn("fix-docs-source-links: docs/API not found, skipping") process.exit(0) } const allContent = getAllMdContent(DOCS_API) const uniquePaths = getUniqueFilePathsFromLinks(allContent) const fileToHash = buildFileToHashMap(uniquePaths) walk(DOCS_API, fileToHash) ================================================ FILE: scripts/generate-compose-type.js ================================================ const { getDeclaration } = require("./generate-shared") let str = `// generated with ${__filename}\n` const minArgs = 2 const maxArgs = 10 const preParam = "name: string, " const returnTypeTransform = (rt) => { // [['PA', 'PB', 'PC'], ['OA', 'OB', 'OC'], ['FCA', 'FCB', 'FCC'], ['FSA', 'FSB', 'FSC']] // -> // [['PA', 'PB', 'PC'], no change // ['OA', 'OB', 'OC'], no change // ['_CustomJoin>'] // ['_CustomJoin>']] const [props, others, fixedC, fixedS] = rt function customJoin(left) { if (left.length === 1) { return left[0] } const [a, ...rest] = left return `_CustomJoin<${a}, ${customJoin(rest)}>` } return [props, others, [customJoin(fixedC)], [customJoin(fixedS)]] } for (let i = minArgs; i < maxArgs; i++) { str += getDeclaration( "compose", "IModelType", ["P", "O", "FC", "FS"], i, preParam, "&", "IModelType", returnTypeTransform ) str += getDeclaration( "compose", "IModelType", ["P", "O", "FC", "FS"], i, null, "&", "IModelType", returnTypeTransform ) } console.log(str) ================================================ FILE: scripts/generate-shared.js ================================================ const alfa = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" function getTemplateVar(templateVar, argNumber) { return `${templateVar}${alfa[argNumber]}` } function getTemplateVars(templateVars, argNumber) { return templateVars.map((tv) => getTemplateVar(tv, argNumber)) } exports.getDeclaration = function getDeclaration( funcName, type, templateVars, args, preParam, operationChar, outType = type, allReturnTypesTransform = (x) => x ) { let str = "// prettier-ignore\n" let allTemplateVars = [] for (let i = 0; i < args; i++) { allTemplateVars = allTemplateVars.concat(getTemplateVars(templateVars, i)) } allTemplateVars = allTemplateVars.map((tv) => tv.startsWith("P") ? `${tv} extends ModelProperties` : tv ) str += `export function ${funcName}<${allTemplateVars.join(", ")}>(` if (preParam) { str += preParam } const allParams = [] for (let i = 0; i < args; i++) { allParams.push(`${alfa[i]}: ${type}<${getTemplateVars(templateVars, i).join(", ")}>`) } str += `${allParams.join(", ")}` str += ")" let allReturnTypes = [] for (const templateVar of templateVars) { let union = [] for (let i = 0; i < args; i++) { union.push(getTemplateVar(templateVar, i)) } allReturnTypes.push(union) } allReturnTypes = allReturnTypesTransform(allReturnTypes) str += `: ${outType}<${allReturnTypes.map((u) => u.join(` ${operationChar} `)).join(", ")}>` return str + "\n" } ================================================ FILE: src/core/action.ts ================================================ import { action as mobxAction } from "mobx" import { getStateTreeNode, MstError, argsToArray, IDisposer, getRoot, Hook, IAnyStateTreeNode, warnError, AnyObjectNode, devMode, IActionContext } from "../internal" export type IMiddlewareEventType = | "action" | "flow_spawn" | "flow_resume" | "flow_resume_error" | "flow_return" | "flow_throw" // | "task_spawn TODO, see #273" export interface IMiddlewareEvent extends IActionContext { /** Event type */ readonly type: IMiddlewareEventType /** Parent event unique id */ readonly parentId: number /** Parent event object */ readonly parentEvent: IMiddlewareEvent | undefined /** Root event unique id */ readonly rootId: number /** Id of all events, from root until current (excluding current) */ readonly allParentIds: number[] } export interface FunctionWithFlag extends Function { _isMSTAction?: boolean _isFlowAction?: boolean } /** * @internal * @hidden */ export type IMiddleware = { handler: IMiddlewareHandler includeHooks: boolean } export type IMiddlewareHandler = ( actionCall: IMiddlewareEvent, next: (actionCall: IMiddlewareEvent, callback?: (value: any) => any) => void, abort: (value: any) => void ) => any let nextActionId = 1 let currentActionContext: IMiddlewareEvent | undefined /** * @internal * @hidden */ export function getCurrentActionContext() { return currentActionContext } /** * @internal * @hidden */ export function getNextActionId() { return nextActionId++ } /** * @internal * @hidden should only ever be used for testing within MST itself */ export function resetNextActionId() { if (!devMode() && !process.env.MST_TESTING) { throw new Error("resetNextActionId should only be used when testing MST") } nextActionId = 1 } // TODO: optimize away entire action context if there is no middleware in tree? /** * @internal * @hidden */ export function runWithActionContext(context: IMiddlewareEvent, fn: Function) { const node = getStateTreeNode(context.context) if (context.type === "action") { node.assertAlive({ actionContext: context }) } const baseIsRunningAction = node._isRunningAction node._isRunningAction = true const previousContext = currentActionContext currentActionContext = context try { return runMiddleWares(node, context, fn) } finally { currentActionContext = previousContext node._isRunningAction = baseIsRunningAction } } /** * @internal * @hidden */ export function getParentActionContext(parentContext: IMiddlewareEvent | undefined) { if (!parentContext) return undefined if (parentContext.type === "action") return parentContext return parentContext.parentActionEvent } /** * @internal * @hidden */ export function createActionInvoker( target: IAnyStateTreeNode, name: string, fn: T ) { const res = function () { const id = getNextActionId() const parentContext = currentActionContext const parentActionContext = getParentActionContext(parentContext) return runWithActionContext( { type: "action", name, id, args: argsToArray(arguments), context: target, tree: getRoot(target), rootId: parentContext ? parentContext.rootId : id, parentId: parentContext ? parentContext.id : 0, allParentIds: parentContext ? [...parentContext.allParentIds, parentContext.id] : [], parentEvent: parentContext, parentActionEvent: parentActionContext }, fn ) } ;(res as FunctionWithFlag)._isMSTAction = true ;(res as FunctionWithFlag)._isFlowAction = fn._isFlowAction return res } /** * Middleware can be used to intercept any action is invoked on the subtree where it is attached. * If a tree is protected (by default), this means that any mutation of the tree will pass through your middleware. * * For more details, see the [middleware docs](concepts/middleware.md) * * @param target Node to apply the middleware to. * @param middleware Middleware to apply. * @returns A callable function to dispose the middleware. */ export function addMiddleware( target: IAnyStateTreeNode, handler: IMiddlewareHandler, includeHooks: boolean = true ): IDisposer { const node = getStateTreeNode(target) if (devMode()) { if (!node.isProtectionEnabled) { warnError( "It is recommended to protect the state tree before attaching action middleware, as otherwise it cannot be guaranteed that all changes are passed through middleware. See `protect`" ) } } return node.addMiddleWare(handler, includeHooks) } /** * Binds middleware to a specific action. * * Example: * ```ts * type.actions(self => { * function takeA____() { * self.toilet.donate() * self.wipe() * self.wipe() * self.toilet.flush() * } * return { * takeA____: decorate(atomic, takeA____) * } * }) * ``` * * @param handler * @param fn * @param includeHooks * @returns The original function */ export function decorate( handler: IMiddlewareHandler, fn: T, includeHooks = true ): T { const middleware: IMiddleware = { handler, includeHooks } ;(fn as any).$mst_middleware = (fn as any).$mst_middleware || [] ;(fn as any).$mst_middleware.push(middleware) return fn } class CollectedMiddlewares { private arrayIndex = 0 private inArrayIndex = 0 private middlewares: IMiddleware[][] = [] constructor(node: AnyObjectNode, fn: Function) { // we just push middleware arrays into an array of arrays to avoid making copies if ((fn as any).$mst_middleware) { this.middlewares.push((fn as any).$mst_middleware) } let n: AnyObjectNode | null = node // Find all middlewares. Optimization: cache this? while (n) { if (n.middlewares) this.middlewares.push(n.middlewares) n = n.parent } } get isEmpty() { return this.middlewares.length <= 0 } getNextMiddleware(): IMiddleware | undefined { const array = this.middlewares[this.arrayIndex] if (!array) return undefined const item = array[this.inArrayIndex++] if (!item) { this.arrayIndex++ this.inArrayIndex = 0 return this.getNextMiddleware() } return item } } function runMiddleWares( node: AnyObjectNode, baseCall: IMiddlewareEvent, originalFn: Function ): any { const middlewares = new CollectedMiddlewares(node, originalFn) // Short circuit if (middlewares.isEmpty) return mobxAction(originalFn).apply(null, baseCall.args) let result: any = null function runNextMiddleware(call: IMiddlewareEvent): any { const middleware = middlewares.getNextMiddleware() const handler = middleware && middleware.handler if (!handler) { return mobxAction(originalFn).apply(null, call.args) } // skip hooks if asked to if (!middleware!.includeHooks && (Hook as any)[call.name]) { return runNextMiddleware(call) } let nextInvoked = false function next(call2: IMiddlewareEvent, callback?: (value: any) => any): void { nextInvoked = true // the result can contain // - the non manipulated return value from an action // - the non manipulated abort value // - one of the above but manipulated through the callback function result = runNextMiddleware(call2) if (callback) { result = callback(result) } } let abortInvoked = false function abort(value: any) { abortInvoked = true // overwrite the result // can be manipulated through middlewares earlier in the queue using the callback fn result = value } handler(call, next, abort) if (devMode()) { if (!nextInvoked && !abortInvoked) { const node2 = getStateTreeNode(call.tree) throw new MstError( `Neither the next() nor the abort() callback within the middleware ${handler.name} for the action: "${call.name}" on the node: ${node2.type.name} was invoked.` ) } else if (nextInvoked && abortInvoked) { const node2 = getStateTreeNode(call.tree) throw new MstError( `The next() and abort() callback within the middleware ${handler.name} for the action: "${call.name}" on the node: ${node2.type.name} were invoked.` ) } } return result } return runNextMiddleware(baseCall) } ================================================ FILE: src/core/actionContext.ts ================================================ import { IAnyStateTreeNode, IMiddlewareEvent } from "../internal" import { getCurrentActionContext } from "./action" export interface IActionContext { /** Event name (action name for actions) */ readonly name: string /** Event unique id */ readonly id: number /** Parent action event object */ readonly parentActionEvent: IMiddlewareEvent | undefined /** Event context (node where the action was invoked) */ readonly context: IAnyStateTreeNode /** Event tree (root node of the node where the action was invoked) */ readonly tree: IAnyStateTreeNode /** Event arguments in an array (action arguments for actions) */ readonly args: any[] } /** * Returns the currently executing MST action context, or undefined if none. */ export function getRunningActionContext(): IActionContext | undefined { let current = getCurrentActionContext() while (current && current.type !== "action") { current = current.parentActionEvent } return current } function _isActionContextThisOrChildOf( actionContext: IActionContext, sameOrParent: number | IActionContext | IMiddlewareEvent, includeSame: boolean ) { const parentId = typeof sameOrParent === "number" ? sameOrParent : sameOrParent.id let current: IActionContext | IMiddlewareEvent | undefined = includeSame ? actionContext : actionContext.parentActionEvent while (current) { if (current.id === parentId) { return true } current = current.parentActionEvent } return false } /** * Returns if the given action context is a parent of this action context. */ export function isActionContextChildOf( actionContext: IActionContext, parent: number | IActionContext | IMiddlewareEvent ) { return _isActionContextThisOrChildOf(actionContext, parent, false) } /** * Returns if the given action context is this or a parent of this action context. */ export function isActionContextThisOrChildOf( actionContext: IActionContext, parentOrThis: number | IActionContext | IMiddlewareEvent ) { return _isActionContextThisOrChildOf(actionContext, parentOrThis, true) } ================================================ FILE: src/core/flow.ts ================================================ import { argsToArray, MstError, setImmediateWithFallback } from "../utils" import { FunctionWithFlag, getCurrentActionContext, getNextActionId, getParentActionContext, IMiddlewareEventType, runWithActionContext } from "./action" /** * @hidden */ export type FlowReturn = R extends Promise ? T : R /** * See [asynchronous actions](concepts/async-actions.md). * * @returns The flow as a promise. */ export function flow( generator: (...args: Args) => Generator, R, any> ): (...args: Args) => Promise> { return createFlowSpawner(generator.name, generator) as any } /** * @deprecated Not needed since TS3.6. * Used for TypeScript to make flows that return a promise return the actual promise result. * * @param val * @returns */ export function castFlowReturn(val: T): T { return val as any } /** * @experimental * experimental api - might change on minor/patch releases * * Convert a promise-returning function to a generator-returning one. * This is intended to allow for usage of `yield*` in async actions to * retain the promise return type. * * Example: * ```ts * function getDataAsync(input: string): Promise { ... } * const getDataGen = toGeneratorFunction(getDataAsync); * * const someModel.actions(self => ({ * someAction: flow(function*() { * // value is typed as number * const value = yield* getDataGen("input value"); * ... * }) * })) * ``` */ export function toGeneratorFunction(p: (...args: Args) => Promise) { return function* (...args: Args) { return (yield p(...args)) as R } } /** * @experimental * experimental api - might change on minor/patch releases * * Convert a promise to a generator yielding that promise * This is intended to allow for usage of `yield*` in async actions to * retain the promise return type. * * Example: * ```ts * function getDataAsync(input: string): Promise { ... } * * const someModel.actions(self => ({ * someAction: flow(function*() { * // value is typed as number * const value = yield* toGenerator(getDataAsync("input value")); * ... * }) * })) * ``` */ export function* toGenerator(p: Promise) { return (yield p) as R } /** * @internal * @hidden */ export function createFlowSpawner(name: string, generator: FunctionWithFlag) { const spawner = function flowSpawner(this: any) { // Implementation based on https://github.com/tj/co/blob/master/index.js const runId = getNextActionId() const parentContext = getCurrentActionContext()! if (!parentContext) { throw new MstError("a mst flow must always have a parent context") } const parentActionContext = getParentActionContext(parentContext) if (!parentActionContext) { throw new MstError("a mst flow must always have a parent action context") } const contextBase = { name, id: runId, tree: parentContext.tree, context: parentContext.context, parentId: parentContext.id, allParentIds: [...parentContext.allParentIds, parentContext.id], rootId: parentContext.rootId, parentEvent: parentContext, parentActionEvent: parentActionContext } const args = arguments function wrap(fn: any, type: IMiddlewareEventType, arg: any) { fn.$mst_middleware = (spawner as any).$mst_middleware // pick up any middleware attached to the flow return runWithActionContext( { ...contextBase, type, args: [arg] }, fn ) } return new Promise(function (resolve, reject) { let gen: any const init = function asyncActionInit() { gen = generator.apply(null, arguments) onFulfilled(undefined) // kick off the flow } ;(init as any).$mst_middleware = (spawner as any).$mst_middleware runWithActionContext( { ...contextBase, type: "flow_spawn", args: argsToArray(args) }, init ) function onFulfilled(res: any) { let ret try { // prettier-ignore const cancelError: any = wrap((r: any) => { ret = gen.next(r) }, "flow_resume", res) if (cancelError instanceof Error) { ret = gen.throw(cancelError) } } catch (e) { // prettier-ignore setImmediateWithFallback(() => { wrap((r: any) => { reject(e) }, "flow_throw", e) }) return } next(ret) return } function onRejected(err: any) { let ret try { // prettier-ignore wrap((r: any) => { ret = gen.throw(r) }, "flow_resume_error", err) // or yieldError? } catch (e) { // prettier-ignore setImmediateWithFallback(() => { wrap((r: any) => { reject(e) }, "flow_throw", e) }) return } next(ret) } function next(ret: any) { if (ret.done) { // prettier-ignore setImmediateWithFallback(() => { wrap((r: any) => { resolve(r) }, "flow_return", ret.value) }) return } // TODO: support more type of values? See https://github.com/tj/co/blob/249bbdc72da24ae44076afd716349d2089b31c4c/index.js#L100 if (!ret.value || typeof ret.value.then !== "function") { // istanbul ignore next throw new MstError("Only promises can be yielded to `async`, got: " + ret) } return ret.value.then(onFulfilled, onRejected) } }) } ;(spawner as FunctionWithFlag)._isFlowAction = true return spawner } ================================================ FILE: src/core/json-patch.ts ================================================ import { MstError, stringStartsWith } from "../internal" /** * https://tools.ietf.org/html/rfc6902 * http://jsonpatch.com/ */ export interface IJsonPatch { readonly op: "replace" | "add" | "remove" readonly path: string readonly value?: any } export interface IReversibleJsonPatch extends IJsonPatch { readonly oldValue: any // This goes beyond JSON-patch, but makes sure each patch can be inverse applied } /** * @internal * @hidden */ export function splitPatch(patch: IReversibleJsonPatch): [IJsonPatch, IJsonPatch] { if (!("oldValue" in patch)) throw new MstError(`Patches without \`oldValue\` field cannot be inversed`) return [stripPatch(patch), invertPatch(patch)] } /** * @internal * @hidden */ export function stripPatch(patch: IReversibleJsonPatch): IJsonPatch { // strips `oldvalue` information from the patch, so that it becomes a patch conform the json-patch spec // this removes the ability to undo the patch switch (patch.op) { case "add": return { op: "add", path: patch.path, value: patch.value } case "remove": return { op: "remove", path: patch.path } case "replace": return { op: "replace", path: patch.path, value: patch.value } } } function invertPatch(patch: IReversibleJsonPatch): IJsonPatch { switch (patch.op) { case "add": return { op: "remove", path: patch.path } case "remove": return { op: "add", path: patch.path, value: patch.oldValue } case "replace": return { op: "replace", path: patch.path, value: patch.oldValue } } } /** * Simple simple check to check it is a number. */ function isNumber(x: string): boolean { return typeof x === "number" } /** * Escape slashes and backslashes. * * http://tools.ietf.org/html/rfc6901 */ export function escapeJsonPath(path: string): string { if (isNumber(path) === true) { return "" + path } if (path.indexOf("/") === -1 && path.indexOf("~") === -1) return path return path.replace(/~/g, "~0").replace(/\//g, "~1") } /** * Unescape slashes and backslashes. */ export function unescapeJsonPath(path: string): string { return path.replace(/~1/g, "/").replace(/~0/g, "~") } /** * Generates a json-path compliant json path from path parts. * * @param path * @returns */ export function joinJsonPath(path: string[]): string { // `/` refers to property with an empty name, while `` refers to root itself! if (path.length === 0) return "" const getPathStr = (p: string[]) => p.map(escapeJsonPath).join("/") if (path[0] === "." || path[0] === "..") { // relative return getPathStr(path) } else { // absolute return "/" + getPathStr(path) } } /** * Splits and decodes a json path into several parts. * * @param path * @returns */ export function splitJsonPath(path: string): string[] { // `/` refers to property with an empty name, while `` refers to root itself! const parts = path.split("/").map(unescapeJsonPath) const valid = path === "" || path === "." || path === ".." || stringStartsWith(path, "/") || stringStartsWith(path, "./") || stringStartsWith(path, "../") if (!valid) { throw new MstError( `a json path must be either rooted, empty or relative, but got '${path}'` ) } // '/a/b/c' -> ["a", "b", "c"] // '../../b/c' -> ["..", "..", "b", "c"] // '' -> [] // '/' -> [''] // './a' -> [".", "a"] // /./a' -> [".", "a"] equivalent to './a' if (parts[0] === "") { parts.shift() } return parts } ================================================ FILE: src/core/mst-operations.ts ================================================ import { isComputedProp, isObservableProp } from "mobx" import { IAnyStateTreeNode, IType, IAnyModelType, getStateTreeNode, IStateTreeNode, isStateTreeNode, IJsonPatch, splitJsonPath, asArray, MstError, IDisposer, resolveNodeByPath, getRelativePathBetweenNodes, freeze, IAnyType, isModelType, InvalidReferenceError, normalizeIdentifier, ReferenceIdentifier, AnyObjectNode, assertIsType, assertIsStateTreeNode, TypeOfValue, assertIsFunction, assertIsNumber, assertIsString, assertArg, assertIsValidIdentifier, IActionContext, getRunningActionContext, IAnyComplexType } from "../internal" /** @hidden */ export type TypeOrStateTreeNodeToStateTreeNode = T extends IType ? TT & IStateTreeNode : T /** * Returns the _actual_ type of the given tree node. (Or throws) * * @param object * @returns */ export function getType(object: IAnyStateTreeNode): IAnyComplexType { assertIsStateTreeNode(object, 1) return getStateTreeNode(object).type } /** * Returns the _declared_ type of the given sub property of an object, array or map. * In the case of arrays and maps the property name is optional and will be ignored. * * Example: * ```ts * const Box = types.model({ x: 0, y: 0 }) * const box = Box.create() * * console.log(getChildType(box, "x").name) // 'number' * ``` * * @param object * @param propertyName * @returns */ export function getChildType(object: IAnyStateTreeNode, propertyName?: string): IAnyType { assertIsStateTreeNode(object, 1) return getStateTreeNode(object).getChildType(propertyName) } /** * Registers a function that will be invoked for each mutation that is applied to the provided model instance, or to any of its children. * See [patches](https://github.com/mobxjs/mobx-state-tree#patches) for more details. onPatch events are emitted immediately and will not await the end of a transaction. * Patches can be used to deeply observe a model tree. * * @param target the model instance from which to receive patches * @param callback the callback that is invoked for each patch. The reversePatch is a patch that would actually undo the emitted patch * @returns function to remove the listener */ export function onPatch( target: IAnyStateTreeNode, callback: (patch: IJsonPatch, reversePatch: IJsonPatch) => void ): IDisposer { // check all arguments assertIsStateTreeNode(target, 1) assertIsFunction(callback, 2) return getStateTreeNode(target).onPatch(callback) } /** * Registers a function that is invoked whenever a new snapshot for the given model instance is available. * The listener will only be fire at the end of the current MobX (trans)action. * See [snapshots](https://github.com/mobxjs/mobx-state-tree#snapshots) for more details. * * @param target * @param callback * @returns */ export function onSnapshot( target: IStateTreeNode>, callback: (snapshot: S) => void ): IDisposer { // check all arguments assertIsStateTreeNode(target, 1) assertIsFunction(callback, 2) return getStateTreeNode(target).onSnapshot(callback) } /** * Applies a JSON-patch to the given model instance or bails out if the patch couldn't be applied * See [patches](https://github.com/mobxjs/mobx-state-tree#patches) for more details. * * Can apply a single past, or an array of patches. * * @param target * @param patch * @returns */ export function applyPatch( target: IAnyStateTreeNode, patch: IJsonPatch | ReadonlyArray ): void { // check all arguments assertIsStateTreeNode(target, 1) assertArg(patch, p => typeof p === "object", "object or array", 2) getStateTreeNode(target).applyPatches(asArray(patch)) } export interface IPatchRecorder { patches: ReadonlyArray inversePatches: ReadonlyArray reversedInversePatches: ReadonlyArray readonly recording: boolean stop(): void resume(): void replay(target?: IAnyStateTreeNode): void undo(target?: IAnyStateTreeNode): void } /** * Small abstraction around `onPatch` and `applyPatch`, attaches a patch listener to a tree and records all the patches. * Returns a recorder object with the following signature: * * Example: * ```ts * export interface IPatchRecorder { * // the recorded patches * patches: IJsonPatch[] * // the inverse of the recorded patches * inversePatches: IJsonPatch[] * // true if currently recording * recording: boolean * // stop recording patches * stop(): void * // resume recording patches * resume(): void * // apply all the recorded patches on the given target (the original subject if omitted) * replay(target?: IAnyStateTreeNode): void * // reverse apply the recorded patches on the given target (the original subject if omitted) * // stops the recorder if not already stopped * undo(): void * } * ``` * * The optional filter function allows to skip recording certain patches. * * @param subject * @param filter * @returns */ export function recordPatches( subject: IAnyStateTreeNode, filter?: ( patch: IJsonPatch, inversePatch: IJsonPatch, actionContext: IActionContext | undefined ) => boolean ): IPatchRecorder { // check all arguments assertIsStateTreeNode(subject, 1) interface IPatches { patches: IJsonPatch[] reversedInversePatches: IJsonPatch[] inversePatches: IJsonPatch[] } const data: Pick = { patches: [], inversePatches: [] } // we will generate the immutable copy of patches on demand for public consumption const publicData: Partial = {} let disposer: IDisposer | undefined const recorder: IPatchRecorder = { get recording() { return !!disposer }, get patches() { if (!publicData.patches) { publicData.patches = data.patches.slice() } return publicData.patches }, get reversedInversePatches() { if (!publicData.reversedInversePatches) { publicData.reversedInversePatches = data.inversePatches.slice().reverse() } return publicData.reversedInversePatches }, get inversePatches() { if (!publicData.inversePatches) { publicData.inversePatches = data.inversePatches.slice() } return publicData.inversePatches }, stop() { if (disposer) { disposer() disposer = undefined } }, resume() { if (disposer) return disposer = onPatch(subject, (patch, inversePatch) => { // skip patches that are asked to be filtered if there's a filter in place if (filter && !filter(patch, inversePatch, getRunningActionContext())) { return } data.patches.push(patch) data.inversePatches.push(inversePatch) // mark immutable public patches as dirty publicData.patches = undefined publicData.inversePatches = undefined publicData.reversedInversePatches = undefined }) }, replay(target?: IAnyStateTreeNode) { applyPatch(target || subject, data.patches) }, undo(target?: IAnyStateTreeNode) { applyPatch(target || subject, data.inversePatches.slice().reverse()) } } recorder.resume() return recorder } /** * The inverse of `unprotect`. * * @param target */ export function protect(target: IAnyStateTreeNode): void { // check all arguments assertIsStateTreeNode(target, 1) const node = getStateTreeNode(target) if (!node.isRoot) throw new MstError("`protect` can only be invoked on root nodes") node.isProtectionEnabled = true } /** * By default it is not allowed to directly modify a model. Models can only be modified through actions. * However, in some cases you don't care about the advantages (like replayability, traceability, etc) this yields. * For example because you are building a PoC or don't have any middleware attached to your tree. * * In that case you can disable this protection by calling `unprotect` on the root of your tree. * * Example: * ```ts * const Todo = types.model({ * done: false * }).actions(self => ({ * toggle() { * self.done = !self.done * } * })) * * const todo = Todo.create() * todo.done = true // throws! * todo.toggle() // OK * unprotect(todo) * todo.done = false // OK * ``` */ export function unprotect(target: IAnyStateTreeNode): void { // check all arguments assertIsStateTreeNode(target, 1) const node = getStateTreeNode(target) if (!node.isRoot) throw new MstError("`unprotect` can only be invoked on root nodes") node.isProtectionEnabled = false } /** * Returns true if the object is in protected mode, @see protect */ export function isProtected(target: IAnyStateTreeNode): boolean { return getStateTreeNode(target).isProtected } /** * Applies a snapshot to a given model instances. Patch and snapshot listeners will be invoked as usual. * * @param target * @param snapshot * @returns */ export function applySnapshot(target: IStateTreeNode>, snapshot: C) { // check all arguments assertIsStateTreeNode(target, 1) return getStateTreeNode(target).applySnapshot(snapshot) } /** * Calculates a snapshot from the given model instance. The snapshot will always reflect the latest state but use * structural sharing where possible. Doesn't require MobX transactions to be completed. * * @param target * @param applyPostProcess If true (the default) then postProcessSnapshot gets applied. * @returns */ export function getSnapshot( target: IStateTreeNode>, applyPostProcess = true ): S { // check all arguments assertIsStateTreeNode(target, 1) const node = getStateTreeNode(target) if (applyPostProcess) return node.snapshot return freeze(node.type.getSnapshot(node, false)) } /** * Given a model instance, returns `true` if the object has a parent, that is, is part of another object, map or array. * * @param target * @param depth How far should we look upward? 1 by default. * @returns */ export function hasParent(target: IAnyStateTreeNode, depth: number = 1): boolean { // check all arguments assertIsStateTreeNode(target, 1) assertIsNumber(depth, 2, 0) let parent: AnyObjectNode | null = getStateTreeNode(target).parent while (parent) { if (--depth === 0) return true parent = parent.parent } return false } /** * Returns the immediate parent of this object, or throws. * * Note that the immediate parent can be either an object, map or array, and * doesn't necessarily refer to the parent model. * * Please note that in child nodes access to the root is only possible * once the `afterAttach` hook has fired. * * @param target * @param depth How far should we look upward? 1 by default. * @returns */ export function getParent( target: IAnyStateTreeNode, depth = 1 ): TypeOrStateTreeNodeToStateTreeNode { // check all arguments assertIsStateTreeNode(target, 1) assertIsNumber(depth, 2, 0) let d = depth let parent: AnyObjectNode | null = getStateTreeNode(target).parent while (parent) { if (--d === 0) return parent.storedValue as any parent = parent.parent } throw new MstError(`Failed to find the parent of ${getStateTreeNode(target)} at depth ${depth}`) } /** * Given a model instance, returns `true` if the object has a parent of given type, that is, is part of another object, map or array * * @param target * @param type * @returns */ export function hasParentOfType(target: IAnyStateTreeNode, type: IAnyComplexType): boolean { // check all arguments assertIsStateTreeNode(target, 1) assertIsType(type, 2) let parent: AnyObjectNode | null = getStateTreeNode(target).parent while (parent) { if (type.is(parent.storedValue)) return true parent = parent.parent } return false } /** * Returns the target's parent of a given type, or throws. * * @param target * @param type * @returns */ export function getParentOfType( target: IAnyStateTreeNode, type: IT ): IT["Type"] { // check all arguments assertIsStateTreeNode(target, 1) assertIsType(type, 2) let parent: AnyObjectNode | null = getStateTreeNode(target).parent while (parent) { if (type.is(parent.storedValue)) return parent.storedValue parent = parent.parent } throw new MstError(`Failed to find the parent of ${getStateTreeNode(target)} of a given type`) } /** * Given an object in a model tree, returns the root object of that tree. * * Please note that in child nodes access to the root is only possible * once the `afterAttach` hook has fired. * * @param target * @returns */ export function getRoot( target: IAnyStateTreeNode ): TypeOrStateTreeNodeToStateTreeNode { // check all arguments assertIsStateTreeNode(target, 1) return getStateTreeNode(target).root.storedValue } /** * Returns the path of the given object in the model tree * * @param target * @returns */ export function getPath(target: IAnyStateTreeNode): string { // check all arguments assertIsStateTreeNode(target, 1) return getStateTreeNode(target).path } /** * Returns the path of the given object as unescaped string array. * * @param target * @returns */ export function getPathParts(target: IAnyStateTreeNode): string[] { // check all arguments assertIsStateTreeNode(target, 1) return splitJsonPath(getStateTreeNode(target).path) } /** * Returns true if the given object is the root of a model tree. * * @param target * @returns */ export function isRoot(target: IAnyStateTreeNode): boolean { // check all arguments assertIsStateTreeNode(target, 1) return getStateTreeNode(target).isRoot } /** * Resolves a path relatively to a given object. * Returns undefined if no value can be found. * * @param target * @param path escaped json path * @returns */ export function resolvePath(target: IAnyStateTreeNode, path: string): any { // check all arguments assertIsStateTreeNode(target, 1) assertIsString(path, 2) const node = resolveNodeByPath(getStateTreeNode(target), path) return node ? node.value : undefined } /** * Resolves a model instance given a root target, the type and the identifier you are searching for. * Returns undefined if no value can be found. * * @param type * @param target * @param identifier * @returns */ export function resolveIdentifier( type: IT, target: IAnyStateTreeNode, identifier: ReferenceIdentifier ): IT["Type"] | undefined { // check all arguments assertIsType(type, 1) assertIsStateTreeNode(target, 2) assertIsValidIdentifier(identifier, 3) const node = getStateTreeNode(target).root.identifierCache!.resolve( type, normalizeIdentifier(identifier) ) return node?.value } /** * Returns the identifier of the target node. * This is the *string normalized* identifier, which might not match the type of the identifier attribute * * @param target * @returns */ export function getIdentifier(target: IAnyStateTreeNode): string | null { // check all arguments assertIsStateTreeNode(target, 1) return getStateTreeNode(target).identifier } /** * Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns such reference if the check passes, * else it returns undefined. * * @param getter Function to access the reference. * @param checkIfAlive true to also make sure the referenced node is alive (default), false to skip this check. * @returns */ export function tryReference( getter: () => N | null | undefined, checkIfAlive = true ): N | undefined { try { const node = getter() if (node === undefined || node === null) { return undefined } else if (isStateTreeNode(node)) { if (!checkIfAlive) { return node } else { return isAlive(node) ? node : undefined } } else { throw new MstError("The reference to be checked is not one of node, null or undefined") } } catch (e) { if (e instanceof InvalidReferenceError) { return undefined } throw e } } /** * Tests if a reference is valid (pointing to an existing node and optionally if alive) and returns if the check passes or not. * * @param getter Function to access the reference. * @param checkIfAlive true to also make sure the referenced node is alive (default), false to skip this check. * @returns */ export function isValidReference( getter: () => N | null | undefined, checkIfAlive = true ): boolean { try { const node = getter() if (node === undefined || node === null) { return false } else if (isStateTreeNode(node)) { return checkIfAlive ? isAlive(node) : true } else { throw new MstError("The reference to be checked is not one of node, null or undefined") } } catch (e) { if (e instanceof InvalidReferenceError) { return false } throw e } } /** * Try to resolve a given path relative to a given node. * * @param target * @param path * @returns */ export function tryResolve(target: IAnyStateTreeNode, path: string): any { // check all arguments assertIsStateTreeNode(target, 1) assertIsString(path, 2) const node = resolveNodeByPath(getStateTreeNode(target), path, false) if (node === undefined) return undefined try { return node.value } catch (e) { // For what ever reason not resolvable (e.g. totally not existing path, or value that cannot be fetched) // see test / issue: 'try resolve doesn't work #686' return undefined } } /** * Given two state tree nodes that are part of the same tree, * returns the shortest jsonpath needed to navigate from the one to the other * * @param base * @param target * @returns */ export function getRelativePath(base: IAnyStateTreeNode, target: IAnyStateTreeNode): string { // check all arguments assertIsStateTreeNode(base, 1) assertIsStateTreeNode(target, 2) return getRelativePathBetweenNodes(getStateTreeNode(base), getStateTreeNode(target)) } /** * Returns a deep copy of the given state tree node as new tree. * Shorthand for `snapshot(x) = getType(x).create(getSnapshot(x))` * * _Tip: clone will create a literal copy, including the same identifiers. To modify identifiers etc. during cloning, don't use clone but take a snapshot of the tree, modify it, and create new instance_ * * @param source * @param keepEnvironment indicates whether the clone should inherit the same environment (`true`, the default), or not have an environment (`false`). If an object is passed in as second argument, that will act as the environment for the cloned tree. * @returns */ export function clone( source: T, keepEnvironment: boolean | any = true ): T { // check all arguments assertIsStateTreeNode(source, 1) const node = getStateTreeNode(source) return node.type.create( node.snapshot, keepEnvironment === true ? node.root.environment : keepEnvironment === false ? undefined : keepEnvironment ) // it's an object or something else } /** * Removes a model element from the state tree, and let it live on as a new state tree */ export function detach(target: T): T { // check all arguments assertIsStateTreeNode(target, 1) getStateTreeNode(target).detach() return target } /** * Removes a model element from the state tree, and mark it as end-of-life; the element should not be used anymore */ export function destroy(target: IAnyStateTreeNode): void { // check all arguments assertIsStateTreeNode(target, 1) const node = getStateTreeNode(target) if (node.isRoot) node.die() else node.parent!.removeChild(node.subpath) } /** * Returns true if the given state tree node is not killed yet. * This means that the node is still a part of a tree, and that `destroy` * has not been called. If a node is not alive anymore, the only thing one can do with it * is requesting it's last path and snapshot * * @param target * @returns */ export function isAlive(target: IAnyStateTreeNode): boolean { // check all arguments assertIsStateTreeNode(target, 1) return getStateTreeNode(target).observableIsAlive } /** * Use this utility to register a function that should be called whenever the * targeted state tree node is destroyed. This is a useful alternative to managing * cleanup methods yourself using the `beforeDestroy` hook. * * This methods returns the same disposer that was passed as argument. * * Example: * ```ts * const Todo = types.model({ * title: types.string * }).actions(self => ({ * afterCreate() { * const autoSaveDisposer = reaction( * () => getSnapshot(self), * snapshot => sendSnapshotToServerSomehow(snapshot) * ) * // stop sending updates to server if this * // instance is destroyed * addDisposer(self, autoSaveDisposer) * } * })) * ``` * * @param target * @param disposer * @returns The same disposer that was passed as argument */ export function addDisposer(target: IAnyStateTreeNode, disposer: IDisposer): IDisposer { // check all arguments assertIsStateTreeNode(target, 1) assertIsFunction(disposer, 2) const node = getStateTreeNode(target) node.addDisposer(disposer) return disposer } /** * Returns the environment of the current state tree, or throws. For more info on environments, * see [Dependency injection](/concepts/dependency-injection) * * Please note that in child nodes access to the root is only possible * once the `afterAttach` hook has fired * * Returns an empty environment if the tree wasn't initialized with an environment * * @param target * @returns */ export function getEnv(target: IAnyStateTreeNode): T { // check all arguments assertIsStateTreeNode(target, 1) const node = getStateTreeNode(target) const env = node.root.environment if (!env) throw new MstError(`Failed to find the environment of ${node} ${node.path}`) return env } /** * Returns whether the current state tree has environment or not. * * @export * @param {IStateTreeNode} target * @return {boolean} */ export function hasEnv(target: IAnyStateTreeNode): boolean { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) throw new MstError( "expected first argument to be a mobx-state-tree node, got " + target + " instead" ) } const node = getStateTreeNode(target) const env = node.root.environment return !!env } /** * Performs a depth first walk through a tree. */ export function walk( target: IAnyStateTreeNode, processor: (item: IAnyStateTreeNode) => void ): void { // check all arguments assertIsStateTreeNode(target, 1) assertIsFunction(processor, 2) const node = getStateTreeNode(target) // tslint:disable-next-line:no_unused-variable node.getChildren().forEach(child => { if (isStateTreeNode(child.storedValue)) walk(child.storedValue, processor) }) processor(node.storedValue) } export interface IModelReflectionPropertiesData { name: string properties: { [K: string]: IAnyType } } /** * Returns a reflection of the model type properties and name for either a model type or model node. * * @param typeOrNode * @returns */ export function getPropertyMembers( typeOrNode: IAnyModelType | IAnyStateTreeNode ): IModelReflectionPropertiesData { let type: IAnyModelType if (isStateTreeNode(typeOrNode)) { type = getType(typeOrNode) as IAnyModelType } else { type = typeOrNode as IAnyModelType } assertArg(type, t => isModelType(t), "model type or model instance", 1) return { name: type.name, properties: { ...type.properties } } } export interface IModelReflectionData extends IModelReflectionPropertiesData { actions: string[] views: string[] volatile: string[] flowActions: string[] } /** * Returns a reflection of the model node, including name, properties, views, volatile state, * and actions. `flowActions` is also provided as a separate array of names for any action that * came from a flow generator as well. * * In the case where a model has two actions: `doSomething` and `doSomethingWithFlow`, where * `doSomethingWithFlow` is a flow generator, the `actions` array will contain both actions, * i.e. ["doSomething", "doSomethingWithFlow"], and the `flowActions` array will contain only * the flow action, i.e. ["doSomethingWithFlow"]. * * @param target * @returns */ export function getMembers(target: IAnyStateTreeNode): IModelReflectionData { const type = getStateTreeNode(target).type as unknown as IAnyModelType const reflected: IModelReflectionData = { ...getPropertyMembers(type), actions: [], volatile: [], views: [], flowActions: [] } const props = Object.getOwnPropertyNames(target) props.forEach(key => { if (key in reflected.properties) return const descriptor = Object.getOwnPropertyDescriptor(target, key)! if (descriptor.get) { if (isComputedProp(target, key)) reflected.views.push(key) else reflected.volatile.push(key) return } if (descriptor.value._isFlowAction === true) { reflected.flowActions.push(key) } if (descriptor.value._isMSTAction === true) { reflected.actions.push(key) } else if (isObservableProp(target, key)) { reflected.volatile.push(key) } else { reflected.views.push(key) } }) return reflected } export function cast( snapshotOrInstance: O ): O export function cast( snapshotOrInstance: | TypeOfValue["CreationType"] | TypeOfValue["SnapshotType"] | TypeOfValue["Type"] ): O /** * Casts a node snapshot or instance type to an instance type so it can be assigned to a type instance. * Note that this is just a cast for the type system, this is, it won't actually convert a snapshot to an instance, * but just fool typescript into thinking so. * Either way, casting when outside an assignation operation won't compile. * * Example: * ```ts * const ModelA = types.model({ * n: types.number * }).actions(self => ({ * setN(aNumber: number) { * self.n = aNumber * } * })) * * const ModelB = types.model({ * innerModel: ModelA * }).actions(self => ({ * someAction() { * // this will allow the compiler to assign a snapshot to the property * self.innerModel = cast({ a: 5 }) * } * })) * ``` * * @param snapshotOrInstance Snapshot or instance * @returns The same object cast as an instance */ export function cast(snapshotOrInstance: any): any { return snapshotOrInstance as any } /** * Casts a node instance type to a snapshot type so it can be assigned to a type snapshot (e.g. to be used inside a create call). * Note that this is just a cast for the type system, this is, it won't actually convert an instance to a snapshot, * but just fool typescript into thinking so. * * Example: * ```ts * const ModelA = types.model({ * n: types.number * }).actions(self => ({ * setN(aNumber: number) { * self.n = aNumber * } * })) * * const ModelB = types.model({ * innerModel: ModelA * }) * * const a = ModelA.create({ n: 5 }); * // this will allow the compiler to use a model as if it were a snapshot * const b = ModelB.create({ innerModel: castToSnapshot(a)}) * ``` * * @param snapshotOrInstance Snapshot or instance * @returns The same object cast as an input (creation) snapshot */ export function castToSnapshot( snapshotOrInstance: I ): Extract extends never ? I : TypeOfValue["CreationType"] { return snapshotOrInstance as any } /** * Casts a node instance type to a reference snapshot type so it can be assigned to a reference snapshot (e.g. to be used inside a create call). * Note that this is just a cast for the type system, this is, it won't actually convert an instance to a reference snapshot, * but just fool typescript into thinking so. * * Example: * ```ts * const ModelA = types.model({ * id: types.identifier, * n: types.number * }).actions(self => ({ * setN(aNumber: number) { * self.n = aNumber * } * })) * * const ModelB = types.model({ * refA: types.reference(ModelA) * }) * * const a = ModelA.create({ id: 'someId', n: 5 }); * // this will allow the compiler to use a model as if it were a reference snapshot * const b = ModelB.create({ refA: castToReferenceSnapshot(a)}) * ``` * * @param instance Instance * @returns The same object cast as a reference snapshot (string or number) */ export function castToReferenceSnapshot( instance: I ): Extract extends never ? I : ReferenceIdentifier { return instance as any } /** * Returns the unique node id (not to be confused with the instance identifier) for a * given instance. * This id is a number that is unique for each instance. * * @export * @param target * @returns */ export function getNodeId(target: IAnyStateTreeNode): number { assertIsStateTreeNode(target, 1) return getStateTreeNode(target).nodeId } ================================================ FILE: src/core/node/BaseNode.ts ================================================ import { AnyObjectNode, NodeLifeCycle, Hook, escapeJsonPath, EventHandlers, IAnyType, IDisposer, devMode, MstError } from "../../internal" import { createAtom, IAtom } from "mobx" type HookSubscribers = { [Hook.afterAttach]: (node: AnyNode, hook: Hook) => void [Hook.afterCreate]: (node: AnyNode, hook: Hook) => void [Hook.afterCreationFinalization]: (node: AnyNode, hook: Hook) => void [Hook.beforeDestroy]: (node: AnyNode, hook: Hook) => void [Hook.beforeDetach]: (node: AnyNode, hook: Hook) => void } /** * @internal * @hidden */ export abstract class BaseNode { private _escapedSubpath?: string private _subpath!: string get subpath() { return this._subpath } private _subpathUponDeath?: string get subpathUponDeath() { return this._subpathUponDeath } private _pathUponDeath?: string protected get pathUponDeath() { return this._pathUponDeath } storedValue!: any // usually the same type as the value, but not always (such as with references) get value(): T { return (this.type as any).getValue(this) } private aliveAtom?: IAtom private _state = NodeLifeCycle.INITIALIZING get state() { return this._state } set state(val: NodeLifeCycle) { const wasAlive = this.isAlive this._state = val const isAlive = this.isAlive if (this.aliveAtom && wasAlive !== isAlive) { this.aliveAtom.reportChanged() } } private _hookSubscribers?: EventHandlers protected abstract fireHook(name: Hook): void protected fireInternalHook(name: Hook) { if (this._hookSubscribers) { this._hookSubscribers.emit(name, this, name) } } registerHook(hook: H, hookHandler: HookSubscribers[H]): IDisposer { if (!this._hookSubscribers) { this._hookSubscribers = new EventHandlers() } return this._hookSubscribers.register(hook, hookHandler) } private _parent!: AnyObjectNode | null get parent() { return this._parent } constructor( readonly type: IAnyType, parent: AnyObjectNode | null, subpath: string, public environment: any ) { this.environment = environment this.baseSetParent(parent, subpath) } getReconciliationType() { return this.type } private pathAtom?: IAtom protected baseSetParent(parent: AnyObjectNode | null, subpath: string) { this._parent = parent this._subpath = subpath this._escapedSubpath = undefined // regenerate when needed if (this.pathAtom) { this.pathAtom.reportChanged() } } /* * Returns (escaped) path representation as string */ get path(): string { return this.getEscapedPath(true) } protected getEscapedPath(reportObserved: boolean): string { if (reportObserved) { if (!this.pathAtom) { this.pathAtom = createAtom(`path`) } this.pathAtom.reportObserved() } if (!this.parent) return "" // regenerate escaped subpath if needed if (this._escapedSubpath === undefined) { this._escapedSubpath = !this._subpath ? "" : escapeJsonPath(this._subpath) } return this.parent.getEscapedPath(reportObserved) + "/" + this._escapedSubpath } get isRoot(): boolean { return this.parent === null } abstract get root(): AnyObjectNode abstract setParent(newParent: AnyObjectNode | null, subpath: string | null): void abstract get snapshot(): S abstract getSnapshot(): S get isAlive() { return this.state !== NodeLifeCycle.DEAD } get isDetaching() { return this.state === NodeLifeCycle.DETACHING } get observableIsAlive() { if (!this.aliveAtom) { this.aliveAtom = createAtom(`alive`) } this.aliveAtom.reportObserved() return this.isAlive } abstract die(): void abstract finalizeCreation(): void protected baseFinalizeCreation(whenFinalized?: () => void) { if (devMode()) { if (!this.isAlive) { // istanbul ignore next throw new MstError( "assertion failed: cannot finalize the creation of a node that is already dead" ) } } // goal: afterCreate hooks runs depth-first. After attach runs parent first, so on afterAttach the parent has completed already if (this.state === NodeLifeCycle.CREATED) { if (this.parent) { if (this.parent.state !== NodeLifeCycle.FINALIZED) { // parent not ready yet, postpone return } this.fireHook(Hook.afterAttach) } this.state = NodeLifeCycle.FINALIZED if (whenFinalized) { whenFinalized() } } } abstract finalizeDeath(): void protected baseFinalizeDeath() { if (this._hookSubscribers) { this._hookSubscribers.clearAll() } this._subpathUponDeath = this._subpath this._pathUponDeath = this.getEscapedPath(false) this.baseSetParent(null, "") this.state = NodeLifeCycle.DEAD } abstract aboutToDie(): void protected baseAboutToDie() { this.fireHook(Hook.beforeDestroy) } } /** * @internal * @hidden */ export type AnyNode = BaseNode ================================================ FILE: src/core/node/Hook.ts ================================================ /** * @hidden */ export enum Hook { afterCreate = "afterCreate", afterAttach = "afterAttach", afterCreationFinalization = "afterCreationFinalization", beforeDetach = "beforeDetach", beforeDestroy = "beforeDestroy" } export interface IHooks { [Hook.afterCreate]?: () => void [Hook.afterAttach]?: () => void [Hook.beforeDetach]?: () => void [Hook.beforeDestroy]?: () => void } export type IHooksGetter = (self: T) => IHooks ================================================ FILE: src/core/node/create-node.ts ================================================ import { MstError, ObjectNode, ScalarNode, AnyNode, getStateTreeNodeSafe, AnyObjectNode, ComplexType, SimpleType } from "../../internal" /** * @internal * @hidden */ export function createObjectNode( type: ComplexType, parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C | T ): ObjectNode { const existingNode = getStateTreeNodeSafe(initialValue) if (existingNode) { if (existingNode.parent) { // istanbul ignore next throw new MstError( `Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '${ parent ? parent.path : "" }/${subpath}', but it lives already at '${existingNode.path}'` ) } if (parent) { existingNode.setParent(parent, subpath) } // else it already has no parent since it is a pre-requisite return existingNode } // not a node, a snapshot return new ObjectNode(type, parent, subpath, environment, initialValue as C) } /** * @internal * @hidden */ export function createScalarNode( type: SimpleType, parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C ): ScalarNode { return new ScalarNode(type, parent, subpath, environment, initialValue) } /** * @internal * @hidden */ export function isNode(value: any): value is AnyNode { return value instanceof ScalarNode || value instanceof ObjectNode } ================================================ FILE: src/core/node/identifier-cache.ts ================================================ import { IObservableArray, values, observable, entries } from "mobx" import { MstError, ObjectNode, mobxShallow, AnyObjectNode, IAnyComplexType } from "../../internal" let identifierCacheId = 0 /** * @internal * @hidden */ export class IdentifierCache { private cacheId = identifierCacheId++ // n.b. in cache all identifiers are normalized to strings private cache = observable.map>() // last time the cache (array) for a given time changed // n.b. it is not really the time, but just an integer that gets increased after each modification to the array private lastCacheModificationPerId = observable.map() constructor() {} private updateLastCacheModificationPerId(identifier: string) { const lcm = this.lastCacheModificationPerId.get(identifier) // we start at 1 since 0 means no update since cache creation this.lastCacheModificationPerId.set(identifier, lcm === undefined ? 1 : lcm + 1) } getLastCacheModificationPerId(identifier: string): string { const modificationId = this.lastCacheModificationPerId.get(identifier) || 0 return `${this.cacheId}-${modificationId}` } addNodeToCache(node: AnyObjectNode, lastCacheUpdate = true): void { if (node.identifierAttribute) { const identifier = node.identifier! if (!this.cache.has(identifier)) { this.cache.set(identifier, observable.array([], mobxShallow)) } const set = this.cache.get(identifier)! if (set.indexOf(node) !== -1) throw new MstError(`Already registered`) set.push(node) if (lastCacheUpdate) { this.updateLastCacheModificationPerId(identifier) } } } mergeCache(node: AnyObjectNode) { values(node.identifierCache!.cache).forEach(nodes => nodes.forEach(child => { this.addNodeToCache(child) }) ) } notifyDied(node: AnyObjectNode) { if (node.identifierAttribute) { const id = node.identifier! const set = this.cache.get(id) if (set) { set.remove(node) // remove empty sets from cache if (!set.length) { this.cache.delete(id) } this.updateLastCacheModificationPerId(node.identifier!) } } } splitCache(splitNode: AnyObjectNode): IdentifierCache { const newCache = new IdentifierCache() // The slash is added here so we only match children of the splitNode. In version 5.1.8 and // earlier there was no trailing slash, so non children that started with the same path string // were being matched incorrectly. const basePath = splitNode.path + "/" entries(this.cache).forEach(([id, nodes]) => { let modified = false for (let i = nodes.length - 1; i >= 0; i--) { const node = nodes[i] if (node === splitNode || node.path.indexOf(basePath) === 0) { newCache.addNodeToCache(node, false) // no need to update lastUpdated since it is a whole new cache nodes.splice(i, 1) // remove empty sets from cache if (!nodes.length) { this.cache.delete(id) } modified = true } } if (modified) { this.updateLastCacheModificationPerId(id) } }) return newCache } has(type: IAnyComplexType, identifier: string): boolean { const set = this.cache.get(identifier) if (!set) return false return set.some(candidate => type.isAssignableFrom(candidate.type)) } resolve( type: IT, identifier: string ): ObjectNode | null { const set = this.cache.get(identifier) if (!set) return null const matches = set.filter(candidate => type.isAssignableFrom(candidate.type)) switch (matches.length) { case 0: return null case 1: return matches[0] default: throw new MstError( `Cannot resolve a reference to type '${ type.name }' with id: '${identifier}' unambigously, there are multiple candidates: ${matches .map(n => n.path) .join(", ")}` ) } } } ================================================ FILE: src/core/node/livelinessChecking.ts ================================================ /** * Defines what MST should do when running into reads / writes to objects that have died. * - `"warn"`: Print a warning (default). * - `"error"`: Throw an exception. * - "`ignore`": Do nothing. */ export type LivelinessMode = "warn" | "error" | "ignore" let livelinessChecking: LivelinessMode = "warn" /** * Defines what MST should do when running into reads / writes to objects that have died. * By default it will print a warning. * Use the `"error"` option to easy debugging to see where the error was thrown and when the offending read / write took place * * @param mode `"warn"`, `"error"` or `"ignore"` */ export function setLivelinessChecking(mode: LivelinessMode) { livelinessChecking = mode } /** * Returns the current liveliness checking mode. * * @returns `"warn"`, `"error"` or `"ignore"` */ export function getLivelinessChecking(): LivelinessMode { return livelinessChecking } /** * @deprecated use LivelinessMode instead * @hidden */ export type LivelynessMode = LivelinessMode /** * @deprecated use setLivelinessChecking instead * @hidden * * Defines what MST should do when running into reads / writes to objects that have died. * By default it will print a warning. * Use the `"error"` option to easy debugging to see where the error was thrown and when the offending read / write took place * * @param mode `"warn"`, `"error"` or `"ignore"` */ export function setLivelynessChecking(mode: LivelinessMode) { setLivelinessChecking(mode) } ================================================ FILE: src/core/node/node-utils.ts ================================================ import { MstError, ObjectNode, splitJsonPath, joinJsonPath, ScalarNode, IChildNodesMap, EMPTY_ARRAY, AnyObjectNode, AnyNode, IAnyType, IType, assertArg, STNValue, Instance, IAnyComplexType } from "../../internal" /** * @internal * @hidden */ export enum NodeLifeCycle { INITIALIZING, // setting up CREATED, // afterCreate has run FINALIZED, // afterAttach has run DETACHING, // being detached from the tree DEAD // no coming back from this one } /** @hidden */ declare const $stateTreeNodeType: unique symbol /** * Common interface that represents a node instance. * @hidden */ export interface IStateTreeNode { /** * @internal */ readonly $treenode?: any // fake, will never be present, just for typing // we use this weird trick to solve an issue with reference types readonly [$stateTreeNodeType]?: [IT] | [any] } /** @hidden */ export type TypeOfValue = T extends IStateTreeNode ? IT : never /** * Represents any state tree node instance. * @hidden */ export interface IAnyStateTreeNode extends STNValue {} /** * Returns true if the given value is a node in a state tree. * More precisely, that is, if the value is an instance of a * `types.model`, `types.array` or `types.map`. * * @param value * @returns true if the value is a state tree node. */ export function isStateTreeNode( value: any ): value is STNValue, IT> { return !!(value && value.$treenode) } /** * @internal * @hidden */ export function assertIsStateTreeNode( value: IAnyStateTreeNode, argNumber: number | number[] ): void { assertArg(value, isStateTreeNode, "mobx-state-tree node", argNumber) } /** * @internal * @hidden */ export function getStateTreeNode(value: IAnyStateTreeNode): AnyObjectNode { if (!isStateTreeNode(value)) { // istanbul ignore next throw new MstError(`Value ${value} is no MST Node`) } return value.$treenode! } /** * @internal * @hidden */ export function getStateTreeNodeSafe(value: IAnyStateTreeNode): AnyObjectNode | null { return (value && value.$treenode) || null } /** * @internal * @hidden */ export function toJSON(this: IStateTreeNode>): S { return getStateTreeNode(this).snapshot } const doubleDot = (_: any) => ".." /** * @internal * @hidden */ export function getRelativePathBetweenNodes(base: AnyObjectNode, target: AnyObjectNode): string { // PRE condition target is (a child of) base! if (base.root !== target.root) { throw new MstError( `Cannot calculate relative path: objects '${base}' and '${target}' are not part of the same object tree` ) } const baseParts = splitJsonPath(base.path) const targetParts = splitJsonPath(target.path) let common = 0 for (; common < baseParts.length; common++) { if (baseParts[common] !== targetParts[common]) break } // TODO: assert that no targetParts paths are "..", "." or ""! return ( baseParts.slice(common).map(doubleDot).join("/") + joinJsonPath(targetParts.slice(common)) ) } /** * @internal * @hidden */ export function resolveNodeByPath( base: AnyObjectNode, path: string, failIfResolveFails: boolean = true ): AnyNode | undefined { return resolveNodeByPathParts(base, splitJsonPath(path), failIfResolveFails) } /** * @internal * @hidden */ export function resolveNodeByPathParts( base: AnyObjectNode, pathParts: string[], failIfResolveFails: boolean = true ): AnyNode | undefined { let current: AnyNode | null = base try { for (let i = 0; i < pathParts.length; i++) { const part = pathParts[i] if (part === "..") { current = current!.parent if (current) continue // not everything has a parent } else if (part === ".") { continue } else if (current) { if (current instanceof ScalarNode) { // check if the value of a scalar resolves to a state tree node (e.g. references) // then we can continue resolving... const value: any = current.value if (isStateTreeNode(value)) { current = getStateTreeNode(value) // fall through } } if (current instanceof ObjectNode) { const subType = current.getChildType(part) if (subType) { current = current.getChildNode(part) if (current) continue } } } throw new MstError( `Could not resolve '${part}' in path '${ joinJsonPath(pathParts.slice(0, i)) || "/" }' while resolving '${joinJsonPath(pathParts)}'` ) } } catch (e) { if (!failIfResolveFails) { return undefined } throw e } return current! } /** * @internal * @hidden */ export function convertChildNodesToArray(childNodes: IChildNodesMap | null): AnyNode[] { if (!childNodes) return EMPTY_ARRAY as AnyNode[] const keys = Object.keys(childNodes) if (!keys.length) return EMPTY_ARRAY as AnyNode[] const result = new Array(keys.length) as AnyNode[] keys.forEach((key, index) => { result[index] = childNodes![key] }) return result } ================================================ FILE: src/core/node/object-node.ts ================================================ // noinspection ES6UnusedImports import { action, computed, IComputedValue, reaction, _allowStateChangesInsideComputed } from "mobx" import { addHiddenFinalProp, ComplexType, convertChildNodesToArray, createActionInvoker, EMPTY_OBJECT, extend, MstError, freeze, IAnyType, IdentifierCache, IDisposer, IJsonPatch, IMiddleware, IMiddlewareHandler, IReversibleJsonPatch, NodeLifeCycle, resolveNodeByPathParts, splitJsonPath, splitPatch, toJSON, EventHandlers, Hook, BaseNode, getLivelinessChecking, normalizeIdentifier, ReferenceIdentifier, IMiddlewareEvent, escapeJsonPath, getPath, warnError, AnyNode, IStateTreeNode, ArgumentTypes, IType, devMode, getCurrentActionContext } from "../../internal" let nextNodeId = 1 const enum ObservableInstanceLifecycle { // the actual observable instance has not been created yet UNINITIALIZED, // the actual observable instance is being created CREATING, // the actual observable instance has been created CREATED } const enum InternalEvents { Dispose = "dispose", Patch = "patch", Snapshot = "snapshot" } /** * @internal * @hidden */ export interface IChildNodesMap { [key: string]: AnyNode } const snapshotReactionOptions = { onError(e: any) { throw e } } type InternalEventHandlers = { [InternalEvents.Dispose]: IDisposer [InternalEvents.Patch]: (patch: IJsonPatch, reversePatch: IJsonPatch) => void [InternalEvents.Snapshot]: (snapshot: S) => void } /** * @internal * @hidden */ export class ObjectNode extends BaseNode { declare readonly type: ComplexType declare storedValue: T & IStateTreeNode> readonly nodeId = ++nextNodeId readonly identifierAttribute?: string readonly identifier: string | null // Identifier is always normalized to string, even if the identifier property isn't readonly unnormalizedIdentifier: ReferenceIdentifier | null identifierCache?: IdentifierCache isProtectionEnabled = true middlewares?: IMiddleware[] hasSnapshotPostProcessor = false private _applyPatches?: (patches: IJsonPatch[]) => void applyPatches(patches: IJsonPatch[]): void { this.createObservableInstanceIfNeeded() this._applyPatches!(patches) } private _applySnapshot?: (snapshot: C) => void applySnapshot(snapshot: C): void { this.createObservableInstanceIfNeeded() this._applySnapshot!(snapshot) } private _autoUnbox = true // unboxing is disabled when reading child nodes _isRunningAction = false // only relevant for root private _hasSnapshotReaction = false private _observableInstanceState = ObservableInstanceLifecycle.UNINITIALIZED private _childNodes: IChildNodesMap private _initialSnapshot: C private _cachedInitialSnapshot?: S private _cachedInitialSnapshotCreated = false private _snapshotComputed: IComputedValue constructor( complexType: ComplexType, parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C ) { super(complexType, parent, subpath, environment) this._snapshotComputed = computed(() => freeze(this.getSnapshot())) this.unbox = this.unbox.bind(this) this._initialSnapshot = freeze(initialValue) this.identifierAttribute = complexType.identifierAttribute if (!parent) { this.identifierCache = new IdentifierCache() } this._childNodes = complexType.initializeChildNodes(this, this._initialSnapshot) // identifier can not be changed during lifecycle of a node // so we safely can read it from initial snapshot this.identifier = null this.unnormalizedIdentifier = null if (this.identifierAttribute && this._initialSnapshot) { let id = (this._initialSnapshot as any)[this.identifierAttribute] if (id === undefined) { // try with the actual node if not (for optional identifiers) const childNode = this._childNodes[this.identifierAttribute] if (childNode) { id = childNode.value } } if (typeof id !== "string" && typeof id !== "number") { throw new MstError( `Instance identifier '${this.identifierAttribute}' for type '${this.type.name}' must be a string or a number` ) } // normalize internal identifier to string this.identifier = normalizeIdentifier(id) this.unnormalizedIdentifier = id } if (!parent) { this.identifierCache!.addNodeToCache(this) } else { parent.root.identifierCache!.addNodeToCache(this) } } createObservableInstanceIfNeeded(fireHooks = true): void { if (this._observableInstanceState === ObservableInstanceLifecycle.UNINITIALIZED) { this.createObservableInstance(fireHooks) } } createObservableInstance(fireHooks = true): void { if (devMode()) { if (this.state !== NodeLifeCycle.INITIALIZING) { // istanbul ignore next throw new MstError( "assertion failed: the creation of the observable instance must be done on the initializing phase" ) } } this._observableInstanceState = ObservableInstanceLifecycle.CREATING // make sure the parent chain is created as well // array with parent chain from parent to child const parentChain = [] let parent = this.parent // for performance reasons we never go back further than the most direct // uninitialized parent // this is done to avoid traversing the whole tree to the root when using // the same reference again while ( parent && parent._observableInstanceState === ObservableInstanceLifecycle.UNINITIALIZED ) { parentChain.unshift(parent) parent = parent.parent } // initialize the uninitialized parent chain from parent to child for (const p of parentChain) { // delay firing hooks until after all parents have been created p.createObservableInstanceIfNeeded(false) } const type = this.type try { this.storedValue = type.createNewInstance(this._childNodes) as typeof this.storedValue this.preboot() this._isRunningAction = true type.finalizeNewInstance(this, this.storedValue) } catch (e) { // short-cut to die the instance, to avoid the snapshot computed starting to throw... this.state = NodeLifeCycle.DEAD throw e } finally { this._isRunningAction = false } this._observableInstanceState = ObservableInstanceLifecycle.CREATED // NOTE: we need to touch snapshot, because non-observable // "_observableInstanceState" field was touched ;(this._snapshotComputed as any).trackAndCompute() if (this.isRoot) this._addSnapshotReaction() this._childNodes = EMPTY_OBJECT this.state = NodeLifeCycle.CREATED if (fireHooks) { this.fireHook(Hook.afterCreate) // Note that the parent might not be finalized at this point // so afterAttach won't be called until later in that case this.finalizeCreation() // fire the hooks of the parents that we created for (const p of parentChain.reverse()) { p.fireHook(Hook.afterCreate) // This will call afterAttach on the child if necessary p.finalizeCreation() } } } get root(): AnyObjectNode { const parent = this.parent return parent ? parent.root : this } clearParent(): void { if (!this.parent) return // detach if attached this.fireHook(Hook.beforeDetach) const previousState = this.state this.state = NodeLifeCycle.DETACHING const root = this.root const newEnv = root.environment const newIdCache = root.identifierCache!.splitCache(this) try { this.parent.removeChild(this.subpath) this.baseSetParent(null, "") this.environment = newEnv this.identifierCache = newIdCache } finally { this.state = previousState } } setParent(newParent: AnyObjectNode, subpath: string): void { const parentChanged = newParent !== this.parent const subpathChanged = subpath !== this.subpath if (!parentChanged && !subpathChanged) { return } if (devMode()) { if (!subpath) { // istanbul ignore next throw new MstError("assertion failed: subpath expected") } if (!newParent) { // istanbul ignore next throw new MstError("assertion failed: new parent expected") } if (this.parent && parentChanged) { throw new MstError( `A node cannot exists twice in the state tree. Failed to add ${this} to path '${newParent.path}/${subpath}'.` ) } if (!this.parent && newParent.root === this) { throw new MstError( `A state tree is not allowed to contain itself. Cannot assign ${this} to path '${newParent.path}/${subpath}'` ) } if ( !this.parent && !!this.environment && this.environment !== newParent.root.environment ) { throw new MstError( `A state tree cannot be made part of another state tree as long as their environments are different.` ) } } if (parentChanged) { // attach to new parent this.environment = undefined // will use root's newParent.root.identifierCache!.mergeCache(this) this.baseSetParent(newParent, subpath) this.fireHook(Hook.afterAttach) } else if (subpathChanged) { // moving to a new subpath on the same parent this.baseSetParent(this.parent, subpath) } } protected fireHook(name: Hook): void { this.fireInternalHook(name) const fn = this.storedValue && typeof this.storedValue === "object" && (this.storedValue as any)[name] if (typeof fn === "function") { // we check for it to allow old mobx peer dependencies that don't have the method to work (even when still bugged) if (_allowStateChangesInsideComputed) { _allowStateChangesInsideComputed(() => { fn.apply(this.storedValue) }) } else { fn.apply(this.storedValue) } } } private _snapshotUponDeath?: S // advantage of using computed for a snapshot is that nicely respects transactions etc. get snapshot(): S { if (this.hasSnapshotPostProcessor) { this.createObservableInstanceIfNeeded() } return this._snapshotComputed.get() } // NOTE: we use this method to get snapshot without creating @computed overhead getSnapshot(): S { if (!this.isAlive) return this._snapshotUponDeath! return this._observableInstanceState === ObservableInstanceLifecycle.CREATED ? this._getActualSnapshot() : this._getCachedInitialSnapshot() } private _getActualSnapshot(): S { return this.type.getSnapshot(this) } private _getCachedInitialSnapshot(): S { if (!this._cachedInitialSnapshotCreated) { const type = this.type const childNodes = this._childNodes const snapshot = this._initialSnapshot this._cachedInitialSnapshot = type.processInitialSnapshot(childNodes, snapshot) this._cachedInitialSnapshotCreated = true } return this._cachedInitialSnapshot! } private isRunningAction(): boolean { if (this._isRunningAction) return true if (this.isRoot) return false return this.parent!.isRunningAction() } assertAlive(context: AssertAliveContext): void { const livelinessChecking = getLivelinessChecking() if (!this.isAlive && livelinessChecking !== "ignore") { const error = this._getAssertAliveError(context) switch (livelinessChecking) { case "error": throw new MstError(error) case "warn": warnError(error) } } } private _getAssertAliveError(context: AssertAliveContext): string { const escapedPath = this.getEscapedPath(false) || this.pathUponDeath || "" const subpath = (context.subpath && escapeJsonPath(context.subpath)) || "" let actionContext = context.actionContext || getCurrentActionContext() // try to use a real action context if possible since it includes the action name if (actionContext && actionContext.type !== "action" && actionContext.parentActionEvent) { actionContext = actionContext.parentActionEvent } let actionFullPath = "" if (actionContext && actionContext.name != null) { // try to use the context, and if it not available use the node one const actionPath = (actionContext && actionContext.context && getPath(actionContext.context)) || escapedPath actionFullPath = `${actionPath}.${actionContext.name}()` } return `You are trying to read or write to an object that is no longer part of a state tree. (Object type: '${this.type.name}', Path upon death: '${escapedPath}', Subpath: '${subpath}', Action: '${actionFullPath}'). Either detach nodes first, or don't use objects after removing / replacing them in the tree.` } getChildNode(subpath: string): AnyNode { this.assertAlive({ subpath }) this._autoUnbox = false try { return this._observableInstanceState === ObservableInstanceLifecycle.CREATED ? this.type.getChildNode(this, subpath) : this._childNodes![subpath] } finally { this._autoUnbox = true } } getChildren(): ReadonlyArray { this.assertAlive(EMPTY_OBJECT) this._autoUnbox = false try { return this._observableInstanceState === ObservableInstanceLifecycle.CREATED ? this.type.getChildren(this) : convertChildNodesToArray(this._childNodes) } finally { this._autoUnbox = true } } getChildType(propertyName?: string): IAnyType { return this.type.getChildType(propertyName) } get isProtected(): boolean { return this.root.isProtectionEnabled } assertWritable(context: AssertAliveContext): void { this.assertAlive(context) if (!this.isRunningAction() && this.isProtected) { throw new MstError( `Cannot modify '${this}', the object is protected and can only be modified by using an action.` ) } } removeChild(subpath: string): void { this.type.removeChild(this, subpath) } // bound on the constructor unbox(childNode: AnyNode | undefined): AnyNode | undefined { if (!childNode) return childNode this.assertAlive({ subpath: childNode.subpath || childNode.subpathUponDeath }) return this._autoUnbox ? childNode.value : childNode } toString(): string { const path = (this.isAlive ? this.path : this.pathUponDeath) || "" const identifier = this.identifier ? `(id: ${this.identifier})` : "" return `${this.type.name}@${path}${identifier}${this.isAlive ? "" : " [dead]"}` } finalizeCreation(): void { this.baseFinalizeCreation(() => { for (let child of this.getChildren()) { child.finalizeCreation() } this.fireInternalHook(Hook.afterCreationFinalization) }) } detach(): void { if (!this.isAlive) throw new MstError(`Error while detaching, node is not alive.`) this.clearParent() } private preboot(): void { const self = this this._applyPatches = createActionInvoker( this.storedValue, "@APPLY_PATCHES", (patches: IJsonPatch[]) => { patches.forEach(patch => { if (!patch.path) { self.type.applySnapshot(self, patch.value) return } const parts = splitJsonPath(patch.path) const node = resolveNodeByPathParts(self, parts.slice(0, -1)) as AnyObjectNode node.applyPatchLocally(parts[parts.length - 1], patch) }) } ) this._applySnapshot = createActionInvoker( this.storedValue, "@APPLY_SNAPSHOT", (snapshot: C) => { // if the snapshot is the same as the current one, avoid performing a reconcile if (snapshot === (self.snapshot as any)) return // else, apply it by calling the type logic return self.type.applySnapshot(self, snapshot as any) } ) addHiddenFinalProp(this.storedValue, "$treenode", this) addHiddenFinalProp(this.storedValue, "toJSON", toJSON) } die(): void { if (!this.isAlive || this.state === NodeLifeCycle.DETACHING) return this.aboutToDie() this.finalizeDeath() } aboutToDie(): void { if (this._observableInstanceState === ObservableInstanceLifecycle.UNINITIALIZED) { return } this.getChildren().forEach(node => { node.aboutToDie() }) // beforeDestroy should run before the disposers since else we could end up in a situation where // a disposer added with addDisposer at this stage (beforeDestroy) is actually never released this.baseAboutToDie() this._internalEventsEmit(InternalEvents.Dispose) this._internalEventsClear(InternalEvents.Dispose) } finalizeDeath(): void { // invariant: not called directly but from "die" this.getChildren().forEach(node => { node.finalizeDeath() }) this.root.identifierCache!.notifyDied(this) // "kill" the computed prop and just store the last snapshot const snapshot = this.snapshot this._snapshotUponDeath = snapshot this._internalEventsClearAll() this.baseFinalizeDeath() } onSnapshot(onChange: (snapshot: S) => void): IDisposer { this._addSnapshotReaction() return this._internalEventsRegister(InternalEvents.Snapshot, onChange) } protected emitSnapshot(snapshot: S): void { this._internalEventsEmit(InternalEvents.Snapshot, snapshot) } onPatch(handler: (patch: IJsonPatch, reversePatch: IJsonPatch) => void): IDisposer { return this._internalEventsRegister(InternalEvents.Patch, handler) } emitPatch(basePatch: IReversibleJsonPatch, source: AnyNode): void { if (this._internalEventsHasSubscribers(InternalEvents.Patch)) { // calculate the relative path of the patch const path = source.path.substr(this.path.length) + (basePatch.path ? "/" + basePatch.path : "") const localizedPatch: IReversibleJsonPatch = extend({}, basePatch, { path }) const [patch, reversePatch] = splitPatch(localizedPatch) this._internalEventsEmit(InternalEvents.Patch, patch, reversePatch) } if (this.parent) this.parent.emitPatch(basePatch, source) } hasDisposer(disposer: () => void): boolean { return this._internalEventsHas(InternalEvents.Dispose, disposer) } addDisposer(disposer: () => void): void { if (!this.hasDisposer(disposer)) { this._internalEventsRegister(InternalEvents.Dispose, disposer, true) return } throw new MstError("cannot add a disposer when it is already registered for execution") } removeDisposer(disposer: () => void): void { if (!this._internalEventsHas(InternalEvents.Dispose, disposer)) { throw new MstError("cannot remove a disposer which was never registered for execution") } this._internalEventsUnregister(InternalEvents.Dispose, disposer) } private removeMiddleware(middleware: IMiddleware): void { if (this.middlewares) { const index = this.middlewares.indexOf(middleware) if (index >= 0) { this.middlewares.splice(index, 1) } } } addMiddleWare(handler: IMiddlewareHandler, includeHooks: boolean = true): IDisposer { const middleware = { handler, includeHooks } if (!this.middlewares) this.middlewares = [middleware] else this.middlewares.push(middleware) return () => { this.removeMiddleware(middleware) } } applyPatchLocally(subpath: string, patch: IJsonPatch): void { this.assertWritable({ subpath }) this.createObservableInstanceIfNeeded() this.type.applyPatchLocally(this, subpath, patch) } private _addSnapshotReaction(): void { if (!this._hasSnapshotReaction) { const snapshotDisposer = reaction( () => this.snapshot, snapshot => this.emitSnapshot(snapshot), snapshotReactionOptions ) this.addDisposer(snapshotDisposer) this._hasSnapshotReaction = true } } // #region internal event handling private _internalEvents?: EventHandlers> // we proxy the methods to avoid creating an EventHandlers instance when it is not needed private _internalEventsHasSubscribers(event: InternalEvents): boolean { return !!this._internalEvents && this._internalEvents.hasSubscribers(event) } private _internalEventsRegister( event: IE, eventHandler: InternalEventHandlers[IE], atTheBeginning = false ): IDisposer { if (!this._internalEvents) { this._internalEvents = new EventHandlers() } return this._internalEvents.register(event, eventHandler, atTheBeginning) } private _internalEventsHas( event: IE, eventHandler: InternalEventHandlers[IE] ): boolean { return !!this._internalEvents && this._internalEvents.has(event, eventHandler) } private _internalEventsUnregister( event: IE, eventHandler: InternalEventHandlers[IE] ): void { if (this._internalEvents) { this._internalEvents.unregister(event, eventHandler) } } private _internalEventsEmit( event: IE, ...args: ArgumentTypes[IE]> ): void { if (this._internalEvents) { this._internalEvents.emit(event, ...args) } } private _internalEventsClear(event: InternalEvents): void { if (this._internalEvents) { this._internalEvents.clear(event) } } private _internalEventsClearAll(): void { if (this._internalEvents) { this._internalEvents.clearAll() } } // #endregion } ObjectNode.prototype.createObservableInstance = action( ObjectNode.prototype.createObservableInstance ) ObjectNode.prototype.detach = action(ObjectNode.prototype.detach) ObjectNode.prototype.die = action(ObjectNode.prototype.die) /** * @internal * @hidden */ export type AnyObjectNode = ObjectNode /** * @internal * @hidden */ export interface AssertAliveContext { subpath?: string actionContext?: IMiddlewareEvent } ================================================ FILE: src/core/node/scalar-node.ts ================================================ import { MstError, freeze, NodeLifeCycle, Hook, BaseNode, AnyObjectNode, SimpleType, devMode } from "../../internal" import { action } from "mobx" /** * @internal * @hidden */ export class ScalarNode extends BaseNode { // note about hooks: // - afterCreate is not emmited in scalar nodes, since it would be emitted in the // constructor, before it can be subscribed by anybody // - afterCreationFinalization could be emitted, but there's no need for it right now // - beforeDetach is never emitted for scalar nodes, since they cannot be detached declare readonly type: SimpleType constructor( simpleType: SimpleType, parent: AnyObjectNode | null, subpath: string, environment: any, initialSnapshot: C ) { super(simpleType, parent, subpath, environment) try { this.storedValue = simpleType.createNewInstance(initialSnapshot) } catch (e) { // short-cut to die the instance, to avoid the snapshot computed starting to throw... this.state = NodeLifeCycle.DEAD throw e } this.state = NodeLifeCycle.CREATED // for scalar nodes there's no point in firing this event since it would fire on the constructor, before // anybody can actually register for/listen to it // this.fireHook(Hook.AfterCreate) this.finalizeCreation() } get root(): AnyObjectNode { // future optimization: store root ref in the node and maintain it if (!this.parent) throw new MstError(`This scalar node is not part of a tree`) return this.parent.root } setParent(newParent: AnyObjectNode, subpath: string): void { const parentChanged = this.parent !== newParent const subpathChanged = this.subpath !== subpath if (!parentChanged && !subpathChanged) { return } if (devMode()) { if (!subpath) { // istanbul ignore next throw new MstError("assertion failed: subpath expected") } if (!newParent) { // istanbul ignore next throw new MstError("assertion failed: parent expected") } if (parentChanged) { // istanbul ignore next throw new MstError("assertion failed: scalar nodes cannot change their parent") } } this.environment = undefined // use parent's this.baseSetParent(this.parent, subpath) } get snapshot(): S { return freeze(this.getSnapshot()) } getSnapshot(): S { return this.type.getSnapshot(this) } toString(): string { const path = (this.isAlive ? this.path : this.pathUponDeath) || "" return `${this.type.name}@${path}${this.isAlive ? "" : " [dead]"}` } die(): void { if (!this.isAlive || this.state === NodeLifeCycle.DETACHING) return this.aboutToDie() this.finalizeDeath() } finalizeCreation(): void { this.baseFinalizeCreation() } aboutToDie(): void { this.baseAboutToDie() } finalizeDeath(): void { this.baseFinalizeDeath() } protected fireHook(name: Hook): void { this.fireInternalHook(name) } } ScalarNode.prototype.die = action(ScalarNode.prototype.die) ================================================ FILE: src/core/process.ts ================================================ import { deprecated, flow, createFlowSpawner } from "../internal" // based on: https://github.com/mobxjs/mobx-utils/blob/master/src/async-action.ts /* All contents of this file are deprecated. The term `process` has been replaced with `flow` to avoid conflicts with the global `process` object. Refer to `flow.ts` for any further changes to this implementation. */ const DEPRECATION_MESSAGE = "See https://github.com/mobxjs/mobx-state-tree/issues/399 for more information. " + "Note that the middleware event types starting with `process` now start with `flow`." /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process(generator: () => IterableIterator): () => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process(generator: (a1: A1) => IterableIterator): (a1: A1) => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process( generator: (a1: A1, a2: A2) => IterableIterator ): (a1: A1, a2: A2) => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process( generator: (a1: A1, a2: A2, a3: A3) => IterableIterator ): (a1: A1, a2: A2, a3: A3) => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process( generator: (a1: A1, a2: A2, a3: A3, a4: A4) => IterableIterator ): (a1: A1, a2: A2, a3: A3, a4: A4) => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process( generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => IterableIterator ): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process( generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => IterableIterator ): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process( generator: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => IterableIterator ): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7) => Promise /** * @deprecated has been renamed to `flow()`. * @hidden */ export function process( generator: ( a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8 ) => IterableIterator ): (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8) => Promise /** * @hidden * * @deprecated has been renamed to `flow()`. * See https://github.com/mobxjs/mobx-state-tree/issues/399 for more information. * Note that the middleware event types starting with `process` now start with `flow`. * * @returns {Promise} */ export function process(asyncAction: any): any { deprecated("process", "`process()` has been renamed to `flow()`. " + DEPRECATION_MESSAGE) return flow(asyncAction) } /** * @internal * @hidden */ export function createProcessSpawner(name: string, generator: Function) { deprecated( "process", "`createProcessSpawner()` has been renamed to `createFlowSpawner()`. " + DEPRECATION_MESSAGE ) return createFlowSpawner(name, generator) } ================================================ FILE: src/core/type/type-checker.ts ================================================ import { MstError, EMPTY_ARRAY, isPrimitive, getStateTreeNode, isStateTreeNode, isPrimitiveType, IAnyType, ExtractCSTWithSTN, isTypeCheckingEnabled, devMode } from "../../internal" /** Validation context entry, this is, where the validation should run against which type */ export interface IValidationContextEntry { /** Subpath where the validation should be run, or an empty string to validate it all */ path: string /** Type to validate the subpath against */ type: IAnyType } /** Array of validation context entries */ export type IValidationContext = IValidationContextEntry[] /** Type validation error */ export interface IValidationError { /** Validation context */ context: IValidationContext /** Value that was being validated, either a snapshot or an instance */ value: any /** Error message */ message?: string } /** Type validation result, which is an array of type validation errors */ export type IValidationResult = IValidationError[] function safeStringify(value: any) { try { return JSON.stringify(value) } catch (e) { // istanbul ignore next return `` } } /** * @internal * @hidden */ export function prettyPrintValue(value: any) { return typeof value === "function" ? `` : isStateTreeNode(value) ? `<${value}>` : `\`${safeStringify(value)}\`` } function shortenPrintValue(valueInString: string) { return valueInString.length < 280 ? valueInString : `${valueInString.substring(0, 272)}......${valueInString.substring(valueInString.length - 8)}` } function toErrorString(error: IValidationError): string { const { value } = error const type = error.context[error.context.length - 1].type! const fullPath = error.context .map(({ path }) => path) .filter(path => path.length > 0) .join("/") const pathPrefix = fullPath.length > 0 ? `at path "/${fullPath}" ` : `` const currentTypename = isStateTreeNode(value) ? `value of type ${getStateTreeNode(value).type.name}:` : isPrimitive(value) ? "value" : "snapshot" const isSnapshotCompatible = type && isStateTreeNode(value) && type.is(getStateTreeNode(value).snapshot) return ( `${pathPrefix}${currentTypename} ${prettyPrintValue(value)} is not assignable ${ type ? `to type: \`${type.name}\`` : `` }` + (error.message ? ` (${error.message})` : "") + (type ? isPrimitiveType(type) || isPrimitive(value) ? `.` : `, expected an instance of \`${(type as IAnyType).name}\` or a snapshot like \`${( type as IAnyType ).describe()}\` instead.` + (isSnapshotCompatible ? " (Note that a snapshot of the provided value is compatible with the targeted type)" : "") : `.`) ) } /** * @internal * @hidden */ export function getContextForPath( context: IValidationContext, path: string, type: IAnyType ): IValidationContext { return context.concat([{ path, type }]) } /** * @internal * @hidden */ export function typeCheckSuccess(): IValidationResult { return EMPTY_ARRAY as any } /** * @internal * @hidden */ export function typeCheckFailure( context: IValidationContext, value: any, message?: string ): IValidationResult { return [{ context, value, message }] } /** * @internal * @hidden */ export function flattenTypeErrors(errors: IValidationResult[]): IValidationResult { return errors.reduce((a, i) => a.concat(i), []) } // TODO; doublecheck: typecheck should only needed to be invoked from: type.create and array / map / value.property will change /** * @internal * @hidden */ export function typecheckInternal( type: IAnyType, value: ExtractCSTWithSTN ): void { // runs typeChecking if it is in dev-mode or through a process.env.ENABLE_TYPE_CHECK flag if (isTypeCheckingEnabled()) { typecheck(type, value) } } /** * Run's the typechecker for the given type on the given value, which can be a snapshot or an instance. * Throws if the given value is not according the provided type specification. * Use this if you need typechecks even in a production build (by default all automatic runtime type checks will be skipped in production builds) * * @param type Type to check against. * @param value Value to be checked, either a snapshot or an instance. */ export function typecheck(type: IT, value: ExtractCSTWithSTN): void { const errors = type.validate(value, [{ path: "", type }]) if (errors.length > 0) { throw new MstError(validationErrorsToString(type, value, errors)) } } function validationErrorsToString( type: IT, value: ExtractCSTWithSTN, errors: IValidationError[] ): string | undefined { if (errors.length === 0) { return undefined } return ( `Error while converting ${shortenPrintValue(prettyPrintValue(value))} to \`${ type.name }\`:\n\n ` + errors.map(toErrorString).join("\n ") ) } ================================================ FILE: src/core/type/type.ts ================================================ import { action } from "mobx" import { MstError, isMutable, isStateTreeNode, getStateTreeNode, IValidationContext, IValidationResult, typecheckInternal, typeCheckFailure, typeCheckSuccess, IStateTreeNode, IJsonPatch, getType, ObjectNode, IChildNodesMap, ModelPrimitive, normalizeIdentifier, AnyObjectNode, AnyNode, BaseNode, ScalarNode, getStateTreeNodeSafe, assertArg } from "../../internal" import type { Writable, WritableKeys } from "ts-essentials" /** * @internal * @hidden */ export enum TypeFlags { String = 1, Number = 1 << 1, Boolean = 1 << 2, Date = 1 << 3, Literal = 1 << 4, Array = 1 << 5, Map = 1 << 6, Object = 1 << 7, Frozen = 1 << 8, Optional = 1 << 9, Reference = 1 << 10, Identifier = 1 << 11, Late = 1 << 12, Refinement = 1 << 13, Union = 1 << 14, Null = 1 << 15, Undefined = 1 << 16, Integer = 1 << 17, Custom = 1 << 18, SnapshotProcessor = 1 << 19, Lazy = 1 << 20, Finite = 1 << 21, Float = 1 << 22, BigInt = 1 << 23 } /** * @internal * @hidden */ export const cannotDetermineSubtype = "cannotDetermine" /** * A state tree node value. * @hidden */ export type STNValue = T extends object ? T & IStateTreeNode : T /** @hidden */ const $type: unique symbol = Symbol("$type") type ExcludeReadonly = T extends {} ? T[WritableKeys] : T /** * A type, either complex or simple. */ export interface IType { // fake, will never be present, just for typing /** @hidden */ readonly [$type]: undefined /** * Friendly type name. */ name: string /** * Name of the identifier attribute or null if none. */ readonly identifierAttribute?: string /** * Creates an instance for the type given an snapshot input. * * @returns An instance of that type. */ create(snapshot?: C | ExcludeReadonly, env?: any): this["Type"] /** * Checks if a given snapshot / instance is of the given type. * * @param thing Snapshot or instance to be checked. * @returns true if the value is of the current type, false otherwise. */ is(thing: any): thing is C | this["Type"] /** * Run's the type's typechecker on the given value with the given validation context. * * @param thing Value to be checked, either a snapshot or an instance. * @param context Validation context, an array of { subpaths, subtypes } that should be validated * @returns The validation result, an array with the list of validation errors. */ validate(thing: C | T, context: IValidationContext): IValidationResult /** * Gets the textual representation of the type as a string. */ describe(): string /** * @deprecated use `Instance` instead. * @hidden */ readonly Type: STNValue /** * @deprecated do not use. * @hidden */ readonly TypeWithoutSTN: T /** * @deprecated use `SnapshotOut` instead. * @hidden */ readonly SnapshotType: S /** * @deprecated use `SnapshotIn` instead. * @hidden */ readonly CreationType: C // Internal api's /** * @internal * @hidden */ flags: TypeFlags /** * @internal * @hidden */ isType: true /** * @internal * @hidden */ instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C | T ): BaseNode /** * @internal * @hidden */ reconcile( current: BaseNode, newValue: C | T, parent: AnyObjectNode, subpath: string ): BaseNode /** * @internal * @hidden */ getSnapshot(node: BaseNode, applyPostProcess?: boolean): S /** * @internal * @hidden */ isAssignableFrom(type: IAnyType): boolean /** * @internal * @hidden */ getSubTypes(): IAnyType[] | IAnyType | null | typeof cannotDetermineSubtype } /** * Any kind of type. */ export interface IAnyType extends IType {} /** * A simple type, this is, a type where the instance and the snapshot representation are the same. */ export interface ISimpleType extends IType {} /** @hidden */ export type Primitives = ModelPrimitive | null | undefined /** * A complex type. * @deprecated just for compatibility with old versions, could be deprecated on the next major version * @hidden */ export interface IComplexType extends IType {} /** * Any kind of complex type. */ export interface IAnyComplexType extends IType {} /** @hidden */ export type ExtractCSTWithoutSTN< IT extends { [$type]: undefined; CreationType: any; SnapshotType: any; TypeWithoutSTN: any } > = IT["CreationType"] | IT["SnapshotType"] | IT["TypeWithoutSTN"] /** @hidden */ export type ExtractCSTWithSTN< IT extends { [$type]: undefined; CreationType: any; SnapshotType: any; Type: any } > = IT["CreationType"] | IT["SnapshotType"] | IT["Type"] /** * The instance representation of a given type. */ export type Instance = T extends { [$type]: undefined; Type: any } ? T["Type"] : T /** * The input (creation) snapshot representation of a given type. */ export type SnapshotIn = T extends { [$type]: undefined; CreationType: any } ? T["CreationType"] : T extends IStateTreeNode ? IT["CreationType"] : T /** * The output snapshot representation of a given type. */ export type SnapshotOut = T extends { [$type]: undefined; SnapshotType: any } ? T["SnapshotType"] : T extends IStateTreeNode ? IT["SnapshotType"] : T /** * A type which is equivalent to the union of SnapshotIn and Instance types of a given typeof TYPE or typeof VARIABLE. * For primitives it defaults to the primitive itself. * * For example: * - `SnapshotOrInstance = SnapshotIn | Instance` * - `SnapshotOrInstance = SnapshotIn | Instance` * * Usually you might want to use this when your model has a setter action that sets a property. * * Example: * ```ts * const ModelA = types.model({ * n: types.number * }) * * const ModelB = types.model({ * innerModel: ModelA * }).actions(self => ({ * // this will accept as property both the snapshot and the instance, whichever is preferred * setInnerModel(m: SnapshotOrInstance) { * self.innerModel = cast(m) * } * })) * ``` */ export type SnapshotOrInstance = SnapshotIn | Instance /** * A base type produces a MST node (Node in the state tree) * * @internal * @hidden */ export abstract class BaseType = BaseNode> implements IType { [$type]!: undefined // these are just to make inner types avaialable to inherited classes readonly C!: C readonly S!: S readonly T!: T readonly N!: N readonly isType = true readonly name: string constructor(name: string) { this.name = name } create(snapshot?: C, environment?: any) { typecheckInternal(this, snapshot) return this.instantiate(null, "", environment, snapshot!).value } getSnapshot(node: N, applyPostProcess?: boolean): S { // istanbul ignore next throw new MstError("unimplemented method") } abstract reconcile(current: N, newValue: C | T, parent: AnyObjectNode, subpath: string): N abstract instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C | T ): N declare abstract flags: TypeFlags abstract describe(): string abstract isValidSnapshot(value: C, context: IValidationContext): IValidationResult isAssignableFrom(type: IAnyType): boolean { return type === this } validate(value: C | T, context: IValidationContext): IValidationResult { const node = getStateTreeNodeSafe(value) if (node) { const valueType = getType(value) return this.isAssignableFrom(valueType) ? typeCheckSuccess() : typeCheckFailure(context, value) // it is tempting to compare snapshots, but in that case we should always clone on assignments... } return this.isValidSnapshot(value as C, context) } is(thing: any): thing is any { return this.validate(thing, [{ path: "", type: this }]).length === 0 } get Type(): any { // istanbul ignore next throw new MstError( "Factory.Type should not be actually called. It is just a Type signature that can be used at compile time with Typescript, by using `typeof type.Type`" ) } get TypeWithoutSTN(): any { // istanbul ignore next throw new MstError( "Factory.TypeWithoutSTN should not be actually called. It is just a Type signature that can be used at compile time with Typescript, by using `typeof type.TypeWithoutSTN`" ) } get SnapshotType(): any { // istanbul ignore next throw new MstError( "Factory.SnapshotType should not be actually called. It is just a Type signature that can be used at compile time with Typescript, by using `typeof type.SnapshotType`" ) } get CreationType(): any { // istanbul ignore next throw new MstError( "Factory.CreationType should not be actually called. It is just a Type signature that can be used at compile time with Typescript, by using `typeof type.CreationType`" ) } abstract getSubTypes(): IAnyType[] | IAnyType | null | typeof cannotDetermineSubtype } BaseType.prototype.create = action(BaseType.prototype.create) /** * @internal * @hidden */ export type AnyBaseType = BaseType /** * @internal * @hidden */ export type ExtractNodeType = IT extends BaseType ? N : never /** * A complex type produces a MST node (Node in the state tree) * * @internal * @hidden */ export abstract class ComplexType extends BaseType> { identifierAttribute?: string constructor(name: string) { super(name) } create(snapshot: C = this.getDefaultSnapshot(), environment?: any) { return super.create(snapshot, environment) } getValue(node: this["N"]): T { node.createObservableInstanceIfNeeded() return node.storedValue } abstract getDefaultSnapshot(): C abstract createNewInstance(childNodes: IChildNodesMap): T abstract finalizeNewInstance(node: this["N"], instance: any): void abstract applySnapshot(node: this["N"], snapshot: C): void abstract applyPatchLocally(node: this["N"], subpath: string, patch: IJsonPatch): void abstract processInitialSnapshot(childNodes: IChildNodesMap, snapshot: C): S abstract getChildren(node: this["N"]): ReadonlyArray abstract getChildNode(node: this["N"], key: string): AnyNode abstract getChildType(propertyName?: string): IAnyType abstract initializeChildNodes(node: this["N"], snapshot: any): IChildNodesMap abstract removeChild(node: this["N"], subpath: string): void isMatchingSnapshotId(current: this["N"], snapshot: C): boolean { return ( !current.identifierAttribute || current.identifier === normalizeIdentifier((snapshot as any)[current.identifierAttribute]) ) } private tryToReconcileNode(current: this["N"], newValue: C | T) { if (current.isDetaching) return false if ((current.snapshot as any) === newValue) { // newValue is the current snapshot of the node, noop return true } if (isStateTreeNode(newValue) && getStateTreeNode(newValue) === current) { // the current node is the same as the new one return true } if ( current.type === this && isMutable(newValue) && !isStateTreeNode(newValue) && this.isMatchingSnapshotId(current, newValue as any) ) { // the newValue has no node, so can be treated like a snapshot // we can reconcile current.applySnapshot(newValue as C) return true } return false } reconcile( current: this["N"], newValue: C | T, parent: AnyObjectNode, subpath: string ): this["N"] { const nodeReconciled = this.tryToReconcileNode(current, newValue) if (nodeReconciled) { current.setParent(parent, subpath) return current } // current node cannot be recycled in any way current.die() // noop if detaching // attempt to reuse the new one if (isStateTreeNode(newValue) && this.isAssignableFrom(getType(newValue))) { // newValue is a Node as well, move it here.. const newNode = getStateTreeNode(newValue) newNode.setParent(parent, subpath) return newNode } // nothing to do, we have to create a new node return this.instantiate(parent, subpath, undefined, newValue) } getSubTypes() { return null } } ComplexType.prototype.create = action(ComplexType.prototype.create) /** * @internal * @hidden */ export abstract class SimpleType extends BaseType> { abstract instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C ): this["N"] createNewInstance(snapshot: C): T { return snapshot as any } getValue(node: this["N"]): T { // if we ever find a case where scalar nodes can be accessed without iterating through its parent // uncomment this to make sure the parent chain is created when this is accessed // if (node.parent) { // node.parent.createObservableInstanceIfNeeded() // } return node.storedValue } getSnapshot(node: this["N"]): S { return node.storedValue } reconcile(current: this["N"], newValue: C, parent: AnyObjectNode, subpath: string): this["N"] { // reconcile only if type and value are still the same, and only if the node is not detaching if (!current.isDetaching && current.type === this && current.storedValue === newValue) { return current } const res = this.instantiate(parent, subpath, undefined, newValue) current.die() // noop if detaching return res } getSubTypes() { return null } } /** * Returns if a given value represents a type. * * @param value Value to check. * @returns `true` if the value is a type. */ export function isType(value: any): value is IAnyType { return typeof value === "object" && value && value.isType === true } /** * @internal * @hidden */ export function assertIsType(type: IAnyType, argNumber: number | number[]) { assertArg(type, isType, "mobx-state-tree type", argNumber) } ================================================ FILE: src/index.ts ================================================ // tslint:disable-next-line:no_unused-variable import { IObservableArray, ObservableMap } from "mobx" /* all code is initially loaded through internal, to avoid circular dep issues */ export { type IModelType, type IAnyModelType, type IDisposer, type IMSTMap, type IMapType, type IMSTArray, type IArrayType, type IType, type IAnyType, type ModelPrimitive, type ISimpleType, type IComplexType, type IAnyComplexType, type IReferenceType, type _CustomCSProcessor, type _CustomOrOther, type _CustomJoin, type _NotCustomized, typecheck, escapeJsonPath, unescapeJsonPath, joinJsonPath, splitJsonPath, type IJsonPatch, type IReversibleJsonPatch, decorate, addMiddleware, type IMiddlewareEvent, type IActionTrackingMiddleware2Call, type IMiddlewareHandler, type IMiddlewareEventType, type IActionTrackingMiddlewareHooks, type IActionTrackingMiddleware2Hooks, process, isStateTreeNode, type IStateTreeNode, type IAnyStateTreeNode, flow, castFlowReturn, applyAction, onAction, type IActionRecorder, type ISerializedActionCall, recordActions, createActionTrackingMiddleware, createActionTrackingMiddleware2, setLivelinessChecking, getLivelinessChecking, type LivelinessMode, setLivelynessChecking, // to be deprecated type LivelynessMode, // to be deprecated type ModelSnapshotType, type ModelCreationType, type ModelSnapshotType2, type ModelCreationType2, type ModelInstanceType, type ModelInstanceTypeProps, type ModelPropertiesDeclarationToProperties, type ModelProperties, type ModelPropertiesDeclaration, type ModelActions, type ITypeUnion, type CustomTypeOptions, type UnionOptions, type Instance, type SnapshotIn, type SnapshotOut, type SnapshotOrInstance, type TypeOrStateTreeNodeToStateTreeNode, type UnionStringArray, getType, getChildType, onPatch, onSnapshot, applyPatch, type IPatchRecorder, recordPatches, protect, unprotect, isProtected, applySnapshot, getSnapshot, hasParent, getParent, hasParentOfType, getParentOfType, getRoot, getPath, getPathParts, isRoot, resolvePath, resolveIdentifier, getIdentifier, tryResolve, getRelativePath, clone, detach, destroy, isAlive, addDisposer, hasEnv, getEnv, walk, type IModelReflectionData, type IModelReflectionPropertiesData, type IMaybeIType, type IMaybe, type IMaybeNull, type IOptionalIType, type OptionalDefaultValueOrFunction, type ValidOptionalValue, type ValidOptionalValues, getMembers, getPropertyMembers, type TypeOfValue, cast, castToSnapshot, castToReferenceSnapshot, isType, isArrayType, isFrozenType, isIdentifierType, isLateType, isLiteralType, isMapType, isModelType, isOptionalType, isPrimitiveType, isReferenceType, isRefinementType, isUnionType, tryReference, isValidReference, type OnReferenceInvalidated, type OnReferenceInvalidatedEvent, type ReferenceOptions, type ReferenceOptionsGetSet, type ReferenceOptionsOnInvalidated, type ReferenceIdentifier, type ISnapshotProcessor, type ISnapshotProcessors, getNodeId, type IActionContext, getRunningActionContext, isActionContextChildOf, isActionContextThisOrChildOf, types, types as t, // We do this as a less ambiguous term for the traditional types module, which sometimes gets confused when discussing "types" in general toGeneratorFunction, toGenerator } from "./internal" ================================================ FILE: src/internal.ts ================================================ /* * All imports / exports should be proxied through this file. * Why? It gives us full control over the module load order, preventing circular dependency isses */ export * from "./core/node/livelinessChecking" export * from "./core/node/Hook" export * from "./core/mst-operations" export * from "./core/node/BaseNode" export * from "./core/node/scalar-node" export * from "./core/node/object-node" export * from "./core/type/type" export * from "./middlewares/create-action-tracking-middleware" export * from "./middlewares/createActionTrackingMiddleware2" export * from "./middlewares/on-action" export * from "./core/action" export * from "./core/actionContext" export * from "./core/type/type-checker" export * from "./core/node/identifier-cache" export * from "./core/node/create-node" export * from "./core/node/node-utils" export * from "./core/process" export * from "./core/flow" export * from "./core/json-patch" export * from "./utils" export * from "./types/utility-types/snapshotProcessor" export * from "./types/complex-types/map" export * from "./types/complex-types/array" export * from "./types/complex-types/model" export * from "./types/primitives" export * from "./types/utility-types/literal" export * from "./types/utility-types/refinement" export * from "./types/utility-types/enumeration" export * from "./types/utility-types/union" export * from "./types/utility-types/optional" export * from "./types/utility-types/maybe" export * from "./types/utility-types/late" export * from "./types/utility-types/lazy" export * from "./types/utility-types/frozen" export * from "./types/utility-types/reference" export * from "./types/utility-types/identifier" export * from "./types/utility-types/custom" export * from "./types" ================================================ FILE: src/middlewares/create-action-tracking-middleware.ts ================================================ import { IMiddlewareEvent, IMiddlewareHandler } from "../internal" const runningActions = new Map() export interface IActionTrackingMiddlewareHooks { filter?: (call: IMiddlewareEvent) => boolean onStart: (call: IMiddlewareEvent) => T onResume: (call: IMiddlewareEvent, context: T) => void onSuspend: (call: IMiddlewareEvent, context: T) => void onSuccess: (call: IMiddlewareEvent, context: T, result: any) => void onFail: (call: IMiddlewareEvent, context: T, error: any) => void } /** * Note: Consider migrating to `createActionTrackingMiddleware2`, it is easier to use. * * Convenience utility to create action based middleware that supports async processes more easily. * All hooks are called for both synchronous and asynchronous actions. Except that either `onSuccess` or `onFail` is called * * The create middleware tracks the process of an action (assuming it passes the `filter`). * `onResume` can return any value, which will be passed as second argument to any other hook. This makes it possible to keep state during a process. * * See the `atomic` middleware for an example * * @param hooks * @returns */ export function createActionTrackingMiddleware( hooks: IActionTrackingMiddlewareHooks ): IMiddlewareHandler { return function actionTrackingMiddleware( call: IMiddlewareEvent, next: (actionCall: IMiddlewareEvent) => any, abort: (value: any) => any ) { switch (call.type) { case "action": { if (!hooks.filter || hooks.filter(call) === true) { const context = hooks.onStart(call) hooks.onResume(call, context) runningActions.set(call.id, { call, context, async: false }) try { const res = next(call) hooks.onSuspend(call, context) if (runningActions.get(call.id)!.async === false) { runningActions.delete(call.id) hooks.onSuccess(call, context, res) } return res } catch (e) { runningActions.delete(call.id) hooks.onFail(call, context, e) throw e } } else { return next(call) } } case "flow_spawn": { const root = runningActions.get(call.rootId)! root.async = true return next(call) } case "flow_resume": case "flow_resume_error": { const root = runningActions.get(call.rootId)! hooks.onResume(call, root.context) try { return next(call) } finally { hooks.onSuspend(call, root.context) } } case "flow_throw": { const root = runningActions.get(call.rootId)! runningActions.delete(call.rootId) hooks.onFail(call, root.context, call.args[0]) return next(call) } case "flow_return": { const root = runningActions.get(call.rootId)! runningActions.delete(call.rootId) hooks.onSuccess(call, root.context, call.args[0]) return next(call) } } } } ================================================ FILE: src/middlewares/createActionTrackingMiddleware2.ts ================================================ import { IMiddlewareEvent, IMiddlewareHandler, IActionContext } from "../internal" export interface IActionTrackingMiddleware2Call extends Readonly { env: TEnv | undefined readonly parentCall?: IActionTrackingMiddleware2Call } export interface IActionTrackingMiddleware2Hooks { filter?: (call: IActionTrackingMiddleware2Call) => boolean onStart: (call: IActionTrackingMiddleware2Call) => void onFinish: (call: IActionTrackingMiddleware2Call, error?: any) => void } class RunningAction { private flowsPending = 0 private running = true constructor( public readonly hooks: IActionTrackingMiddleware2Hooks | undefined, readonly call: IActionTrackingMiddleware2Call ) { if (hooks) { hooks.onStart(call) } } finish(error?: any) { if (this.running) { this.running = false if (this.hooks) { this.hooks.onFinish(this.call, error) } } } incFlowsPending() { this.flowsPending++ } decFlowsPending() { this.flowsPending-- } get hasFlowsPending() { return this.flowsPending > 0 } } /** * Convenience utility to create action based middleware that supports async processes more easily. * The flow is like this: * - for each action: if filter passes -> `onStart` -> (inner actions recursively) -> `onFinish` * * Example: if we had an action `a` that called inside an action `b1`, then `b2` the flow would be: * - `filter(a)` * - `onStart(a)` * - `filter(b1)` * - `onStart(b1)` * - `onFinish(b1)` * - `filter(b2)` * - `onStart(b2)` * - `onFinish(b2)` * - `onFinish(a)` * * The flow is the same no matter if the actions are sync or async. * * See the `atomic` middleware for an example * * @param hooks * @returns */ export function createActionTrackingMiddleware2( middlewareHooks: IActionTrackingMiddleware2Hooks ): IMiddlewareHandler { const runningActions = new Map() return function actionTrackingMiddleware( call: IMiddlewareEvent, next: (actionCall: IMiddlewareEvent) => any ) { // find parentRunningAction const parentRunningAction = call.parentActionEvent ? runningActions.get(call.parentActionEvent.id) : undefined if (call.type === "action") { const newCall: IActionTrackingMiddleware2Call = { ...call, // make a shallow copy of the parent action env env: parentRunningAction && parentRunningAction.call.env, parentCall: parentRunningAction && parentRunningAction.call } const passesFilter = !middlewareHooks.filter || middlewareHooks.filter(newCall) const hooks = passesFilter ? middlewareHooks : undefined const runningAction = new RunningAction(hooks, newCall) runningActions.set(call.id, runningAction) let res try { res = next(call) } catch (e) { runningActions.delete(call.id) runningAction.finish(e) throw e } // sync action finished if (!runningAction.hasFlowsPending) { runningActions.delete(call.id) runningAction.finish() } return res } else { if (!parentRunningAction) { return next(call) } switch (call.type) { case "flow_spawn": { parentRunningAction.incFlowsPending() return next(call) } case "flow_resume": case "flow_resume_error": { return next(call) } case "flow_throw": { const error = call.args[0] try { return next(call) } finally { parentRunningAction.decFlowsPending() if (!parentRunningAction.hasFlowsPending) { runningActions.delete(call.parentActionEvent!.id) parentRunningAction.finish(error) } } } case "flow_return": { try { return next(call) } finally { parentRunningAction.decFlowsPending() if (!parentRunningAction.hasFlowsPending) { runningActions.delete(call.parentActionEvent!.id) parentRunningAction.finish() } } } } } } } ================================================ FILE: src/middlewares/on-action.ts ================================================ import { runInAction } from "mobx" import { getStateTreeNode, isStateTreeNode, addMiddleware, tryResolve, applyPatch, getType, applySnapshot, isRoot, isProtected, MstError, isPlainObject, isPrimitive, IDisposer, isArray, asArray, getRelativePathBetweenNodes, IAnyStateTreeNode, warnError, AnyNode, assertIsStateTreeNode, devMode, assertArg, IActionContext, getRunningActionContext } from "../internal" export interface ISerializedActionCall { name: string path?: string args?: any[] } export interface IActionRecorder { actions: ReadonlyArray readonly recording: boolean stop(): void resume(): void replay(target: IAnyStateTreeNode): void } function serializeArgument(node: AnyNode, actionName: string, index: number, arg: any): any { if (arg instanceof Date) return { $MST_DATE: arg.getTime() } if (isPrimitive(arg)) return arg // We should not serialize MST nodes, even if we can, because we don't know if the receiving party can handle a raw snapshot instead of an // MST type instance. So if one wants to serialize a MST node that was pass in, either explitly pass: 1: an id, 2: a (relative) path, 3: a snapshot if (isStateTreeNode(arg)) return serializeTheUnserializable(`[MSTNode: ${getType(arg).name}]`) if (typeof arg === "function") return serializeTheUnserializable(`[function]`) if (typeof arg === "object" && !isPlainObject(arg) && !isArray(arg)) return serializeTheUnserializable( `[object ${ (arg && (arg as any).constructor && (arg as any).constructor.name) || "Complex Object" }]` ) try { // Check if serializable, cycle free etc... // MWE: there must be a better way.... JSON.stringify(arg) // or throws return arg } catch (e) { return serializeTheUnserializable("" + e) } } function deserializeArgument(adm: AnyNode, value: any): any { if (value && typeof value === "object" && "$MST_DATE" in value) return new Date(value["$MST_DATE"]) return value } function serializeTheUnserializable(baseType: string) { return { $MST_UNSERIALIZABLE: true, type: baseType } } /** * Applies an action or a series of actions in a single MobX transaction. * Does not return any value * Takes an action description as produced by the `onAction` middleware. * * @param target * @param actions */ export function applyAction( target: IAnyStateTreeNode, actions: ISerializedActionCall | ISerializedActionCall[] ): void { // check all arguments assertIsStateTreeNode(target, 1) assertArg(actions, a => typeof a === "object", "object or array", 2) runInAction(() => { asArray(actions).forEach(action => baseApplyAction(target, action)) }) } function baseApplyAction(target: IAnyStateTreeNode, action: ISerializedActionCall): any { const resolvedTarget = tryResolve(target, action.path || "") if (!resolvedTarget) throw new MstError(`Invalid action path: ${action.path || ""}`) const node = getStateTreeNode(resolvedTarget) // Reserved functions if (action.name === "@APPLY_PATCHES") { return applyPatch.call(null, resolvedTarget, action.args![0]) } if (action.name === "@APPLY_SNAPSHOT") { return applySnapshot.call(null, resolvedTarget, action.args![0]) } if (!(typeof resolvedTarget[action.name] === "function")) throw new MstError(`Action '${action.name}' does not exist in '${node.path}'`) return resolvedTarget[action.name].apply( resolvedTarget, action.args ? action.args.map(v => deserializeArgument(node, v)) : [] ) } /** * Small abstraction around `onAction` and `applyAction`, attaches an action listener to a tree and records all the actions emitted. * Returns an recorder object with the following signature: * * Example: * ```ts * export interface IActionRecorder { * // the recorded actions * actions: ISerializedActionCall[] * // true if currently recording * recording: boolean * // stop recording actions * stop(): void * // resume recording actions * resume(): void * // apply all the recorded actions on the given object * replay(target: IAnyStateTreeNode): void * } * ``` * * The optional filter function allows to skip recording certain actions. * * @param subject * @returns */ export function recordActions( subject: IAnyStateTreeNode, filter?: (action: ISerializedActionCall, actionContext: IActionContext | undefined) => boolean ): IActionRecorder { // check all arguments assertIsStateTreeNode(subject, 1) const actions: ISerializedActionCall[] = [] const listener = (call: ISerializedActionCall) => { const recordThis = filter ? filter(call, getRunningActionContext()) : true if (recordThis) { actions.push(call) } } let disposer: IDisposer | undefined const recorder: IActionRecorder = { actions, get recording() { return !!disposer }, stop() { if (disposer) { disposer() disposer = undefined } }, resume() { if (disposer) return disposer = onAction(subject, listener) }, replay(target) { applyAction(target, actions) } } recorder.resume() return recorder } /** * Registers a function that will be invoked for each action that is called on the provided model instance, or to any of its children. * See [actions](https://github.com/mobxjs/mobx-state-tree#actions) for more details. onAction events are emitted only for the outermost called action in the stack. * Action can also be intercepted by middleware using addMiddleware to change the function call before it will be run. * * Not all action arguments might be serializable. For unserializable arguments, a struct like `{ $MST_UNSERIALIZABLE: true, type: "someType" }` will be generated. * MST Nodes are considered non-serializable as well (they could be serialized as there snapshot, but it is uncertain whether an replaying party will be able to handle such a non-instantiated snapshot). * Rather, when using `onAction` middleware, one should consider in passing arguments which are 1: an id, 2: a (relative) path, or 3: a snapshot. Instead of a real MST node. * * Example: * ```ts * const Todo = types.model({ * task: types.string * }) * * const TodoStore = types.model({ * todos: types.array(Todo) * }).actions(self => ({ * add(todo) { * self.todos.push(todo); * } * })) * * const s = TodoStore.create({ todos: [] }) * * let disposer = onAction(s, (call) => { * console.log(call); * }) * * s.add({ task: "Grab a coffee" }) * // Logs: { name: "add", path: "", args: [{ task: "Grab a coffee" }] } * ``` * * @param target * @param listener * @param attachAfter (default false) fires the listener *after* the action has executed instead of before. * @returns */ export function onAction( target: IAnyStateTreeNode, listener: (call: ISerializedActionCall) => void, attachAfter = false ): IDisposer { // check all arguments assertIsStateTreeNode(target, 1) if (devMode()) { if (!isRoot(target)) warnError( "Warning: Attaching onAction listeners to non root nodes is dangerous: No events will be emitted for actions initiated higher up in the tree." ) if (!isProtected(target)) warnError( "Warning: Attaching onAction listeners to non protected nodes is dangerous: No events will be emitted for direct modifications without action." ) } return addMiddleware(target, function handler(rawCall, next) { if (rawCall.type === "action" && rawCall.id === rawCall.rootId) { const sourceNode = getStateTreeNode(rawCall.context) const info = { name: rawCall.name, path: getRelativePathBetweenNodes(getStateTreeNode(target), sourceNode), args: rawCall.args.map((arg: any, index: number) => serializeArgument(sourceNode, rawCall.name, index, arg) ) } if (attachAfter) { const res = next(rawCall) listener(info) return res } else { listener(info) return next(rawCall) } } else { return next(rawCall) } }) } ================================================ FILE: src/types/complex-types/array.ts ================================================ import { _getAdministration, action, IArrayDidChange, IArrayWillChange, IArrayWillSplice, intercept, IObservableArray, observable, observe } from "mobx" import { addHiddenFinalProp, addHiddenWritableProp, AnyNode, AnyObjectNode, assertIsType, ComplexType, convertChildNodesToArray, createActionInvoker, createObjectNode, devMode, EMPTY_ARRAY, EMPTY_OBJECT, ExtractCSTWithSTN, MstError, flattenTypeErrors, getContextForPath, getStateTreeNode, IAnyStateTreeNode, IAnyType, IChildNodesMap, IHooksGetter, IJsonPatch, isArray, isNode, isPlainObject, isStateTreeNode, IStateTreeNode, isType, IType, IValidationContext, IValidationResult, mobxShallow, ObjectNode, typeCheckFailure, typecheckInternal, TypeFlags } from "../../internal" /** @hidden */ export interface IMSTArray extends IObservableArray { // needs to be split or else it will complain about not being compatible with the array interface push(...items: IT["Type"][]): number push(...items: ExtractCSTWithSTN[]): number concat(...items: ConcatArray[]): IT["Type"][] concat(...items: ConcatArray>[]): IT["Type"][] concat(...items: (IT["Type"] | ConcatArray)[]): IT["Type"][] concat(...items: (ExtractCSTWithSTN | ConcatArray>)[]): IT["Type"][] splice(start: number, deleteCount?: number): IT["Type"][] splice(start: number, deleteCount: number, ...items: IT["Type"][]): IT["Type"][] splice(start: number, deleteCount: number, ...items: ExtractCSTWithSTN[]): IT["Type"][] unshift(...items: IT["Type"][]): number unshift(...items: ExtractCSTWithSTN[]): number } /** @hidden */ export interface IArrayType extends IType> { hooks(hooks: IHooksGetter>): IArrayType } /** * @internal * @hidden */ export class ArrayType extends ComplexType< readonly IT["CreationType"][] | undefined, IT["SnapshotType"][], IMSTArray > { readonly flags = TypeFlags.Array private readonly hookInitializers: Array>> = [] constructor( name: string, private readonly _subType: IT, hookInitializers: Array>> = [] ) { super(name) this.hookInitializers = hookInitializers } hooks(hooks: IHooksGetter>) { const hookInitializers = this.hookInitializers.length > 0 ? this.hookInitializers.concat(hooks) : [hooks] return new ArrayType(this.name, this._subType, hookInitializers) } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { return createObjectNode(this, parent, subpath, environment, initialValue) } initializeChildNodes(objNode: this["N"], snapshot: this["C"] = []): IChildNodesMap { const subType = (objNode.type as this)._subType const result: IChildNodesMap = {} snapshot.forEach((item, index) => { const subpath = "" + index result[subpath] = subType.instantiate(objNode, subpath, undefined, item) }) return result } createNewInstance(childNodes: IChildNodesMap): this["T"] { const options = { ...mobxShallow, name: this.name } return observable.array(convertChildNodesToArray(childNodes), options) as this["T"] } finalizeNewInstance(node: this["N"], instance: this["T"]): void { _getAdministration(instance).dehancer = node.unbox const type = node.type as this type.hookInitializers.forEach(initializer => { const hooks = initializer(instance) Object.keys(hooks).forEach(name => { const hook = hooks[name as keyof typeof hooks]! const actionInvoker = createActionInvoker(instance as IAnyStateTreeNode, name, hook) ;(!devMode() ? addHiddenFinalProp : addHiddenWritableProp)( instance, name, actionInvoker ) }) }) intercept(instance as IObservableArray, this.willChange) observe(instance as IObservableArray, this.didChange) } describe() { return this.name } getChildren(node: this["N"]): AnyNode[] { return node.storedValue.slice() } getChildNode(node: this["N"], key: string): AnyNode { const index = Number(key) if (index < node.storedValue.length) return node.storedValue[index] throw new MstError("Not a child: " + key) } willChange( change: IArrayWillChange | IArrayWillSplice ): IArrayWillChange | IArrayWillSplice | null { const node = getStateTreeNode(change.object as IStateTreeNode) node.assertWritable({ subpath: "" + change.index }) const subType = (node.type as this)._subType const childNodes = node.getChildren() switch (change.type) { case "update": { if (change.newValue === change.object[change.index]) return null const updatedNodes = reconcileArrayChildren( node, subType, [childNodes[change.index]], [change.newValue], [change.index] ) if (!updatedNodes) { return null } change.newValue = updatedNodes[0] } break case "splice": { const { index, removedCount, added } = change const addedNodes = reconcileArrayChildren( node, subType, childNodes.slice(index, index + removedCount), added, added.map((_, i) => index + i) ) if (!addedNodes) { return null } change.added = addedNodes // update paths of remaining items for (let i = index + removedCount; i < childNodes.length; i++) { childNodes[i].setParent(node, "" + (i + added.length - removedCount)) } } break } return change } getSnapshot(node: this["N"]): this["S"] { return node.getChildren().map(childNode => childNode.snapshot) } processInitialSnapshot(childNodes: IChildNodesMap): this["S"] { const processed: this["S"] = [] Object.keys(childNodes).forEach(key => { processed.push(childNodes[key].getSnapshot()) }) return processed } didChange(change: IArrayDidChange): void { const node = getStateTreeNode(change.object as IAnyStateTreeNode) switch (change.type) { case "update": return void node.emitPatch( { op: "replace", path: "" + change.index, value: change.newValue.snapshot, oldValue: change.oldValue ? change.oldValue.snapshot : undefined }, node ) case "splice": // Perf: emit one patch for clear and replace ops. if (change.removedCount && change.addedCount === change.object.length) { return void node.emitPatch( { op: "replace", path: "", value: node.snapshot, oldValue: change.removed.map(node => node.snapshot) }, node ) } for (let i = change.removedCount - 1; i >= 0; i--) node.emitPatch( { op: "remove", path: "" + (change.index + i), oldValue: change.removed[i].snapshot }, node ) for (let i = 0; i < change.addedCount; i++) node.emitPatch( { op: "add", path: "" + (change.index + i), value: change.added[i].snapshot, oldValue: undefined }, node ) return } } applyPatchLocally(node: this["N"], subpath: string, patch: IJsonPatch): void { const target = node.storedValue const index = subpath === "-" ? target.length : Number(subpath) switch (patch.op) { case "replace": target[index] = patch.value break case "add": target.splice(index, 0, patch.value) break case "remove": target.splice(index, 1) break } } applySnapshot(node: this["N"], snapshot: this["C"]): void { typecheckInternal(this, snapshot) const target = node.storedValue target.replace(snapshot as any) } getChildType(): IAnyType { return this._subType } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (!isArray(value)) { return typeCheckFailure(context, value, "Value is not an array") } return flattenTypeErrors( value.map((item, index) => this._subType.validate(item, getContextForPath(context, "" + index, this._subType)) ) ) } getDefaultSnapshot(): this["C"] { return EMPTY_ARRAY as this["C"] } removeChild(node: this["N"], subpath: string) { node.storedValue.splice(Number(subpath), 1) } } ArrayType.prototype.applySnapshot = action(ArrayType.prototype.applySnapshot) /** * `types.array` - Creates an index based collection type who's children are all of a uniform declared type. * * This type will always produce [observable arrays](https://mobx.js.org/api.html#observablearray) * * Example: * ```ts * const Todo = types.model({ * task: types.string * }) * * const TodoStore = types.model({ * todos: types.array(Todo) * }) * * const s = TodoStore.create({ todos: [] }) * unprotect(s) // needed to allow modifying outside of an action * s.todos.push({ task: "Grab coffee" }) * console.log(s.todos[0]) // prints: "Grab coffee" * ``` * * @param subtype * @returns */ export function array(subtype: IT): IArrayType { assertIsType(subtype, 1) return new ArrayType(`${subtype.name}[]`, subtype) } function reconcileArrayChildren( parent: AnyObjectNode, childType: IType, oldNodes: AnyNode[], newValues: TT[], newPaths: (string | number)[] ): AnyNode[] | null { let nothingChanged = true for (let i = 0; ; i++) { const hasNewNode = i <= newValues.length - 1 const oldNode = oldNodes[i] let newValue = hasNewNode ? newValues[i] : undefined const newPath = "" + newPaths[i] // for some reason, instead of newValue we got a node, fallback to the storedValue // TODO: https://github.com/mobxjs/mobx-state-tree/issues/340#issuecomment-325581681 if (isNode(newValue)) newValue = newValue.storedValue if (!oldNode && !hasNewNode) { // both are empty, end break } else if (!hasNewNode) { // new one does not exists nothingChanged = false oldNodes.splice(i, 1) if (oldNode instanceof ObjectNode) { // since it is going to be returned by pop/splice/shift better create it before killing it // so it doesn't end up in an undead state oldNode.createObservableInstanceIfNeeded() } oldNode.die() i-- } else if (!oldNode) { // there is no old node, create it // check if already belongs to the same parent. if so, avoid pushing item in. only swapping can occur. if (isStateTreeNode(newValue) && getStateTreeNode(newValue).parent === parent) { // this node is owned by this parent, but not in the reconcilable set, so it must be double throw new MstError( `Cannot add an object to a state tree if it is already part of the same or another state tree. Tried to assign an object to '${ parent.path }/${newPath}', but it lives already at '${getStateTreeNode(newValue).path}'` ) } nothingChanged = false const newNode = valueAsNode(childType, parent, newPath, newValue) oldNodes.splice(i, 0, newNode) } else if (areSame(oldNode, newValue)) { // both are the same, reconcile oldNodes[i] = valueAsNode(childType, parent, newPath, newValue, oldNode) } else { // nothing to do, try to reorder let oldMatch = undefined // find a possible candidate to reuse for (let j = i; j < oldNodes.length; j++) { if (areSame(oldNodes[j], newValue)) { oldMatch = oldNodes.splice(j, 1)[0] break } } nothingChanged = false const newNode = valueAsNode(childType, parent, newPath, newValue, oldMatch) oldNodes.splice(i, 0, newNode) } } return nothingChanged ? null : oldNodes } /** * Convert a value to a node at given parent and subpath. Attempts to reuse old node if possible and given. */ function valueAsNode( childType: IAnyType, parent: AnyObjectNode, subpath: string, newValue: any, oldNode?: AnyNode ) { // ensure the value is valid-ish typecheckInternal(childType, newValue) function getNewNode() { // the new value has a MST node if (isStateTreeNode(newValue)) { const childNode = getStateTreeNode(newValue) childNode.assertAlive(EMPTY_OBJECT) // the node lives here if (childNode.parent !== null && childNode.parent === parent) { childNode.setParent(parent, subpath) return childNode } } // there is old node and new one is a value/snapshot if (oldNode) { return childType.reconcile(oldNode, newValue, parent, subpath) } // nothing to do, create from scratch return childType.instantiate(parent, subpath, undefined, newValue) } const newNode = getNewNode() if (oldNode && oldNode !== newNode) { if (oldNode instanceof ObjectNode) { // since it is going to be returned by pop/splice/shift better create it before killing it // so it doesn't end up in an undead state oldNode.createObservableInstanceIfNeeded() } oldNode.die() } return newNode } /** * Check if a node holds a value. */ function areSame(oldNode: AnyNode, newValue: any) { // never consider dead old nodes for reconciliation if (!oldNode.isAlive) { return false } // the new value has the same node if (isStateTreeNode(newValue)) { const newNode = getStateTreeNode(newValue) return newNode.isAlive && newNode === oldNode } // the provided value is the snapshot of the old node if (oldNode.snapshot === newValue) { return true } // Non object nodes don't get reconciled if (!(oldNode instanceof ObjectNode)) { return false } const oldNodeType = oldNode.getReconciliationType() // new value is a snapshot with the correct identifier return ( oldNode.identifier !== null && oldNode.identifierAttribute && isPlainObject(newValue) && oldNodeType.is(newValue) && (oldNodeType as any).isMatchingSnapshotId(oldNode, newValue) ) } /** * Returns if a given value represents an array type. * * @param type * @returns `true` if the type is an array type. */ export function isArrayType(type: unknown): type is IArrayType { return isType(type) && (type.flags & TypeFlags.Array) > 0 } ================================================ FILE: src/types/complex-types/map.ts ================================================ import { _interceptReads, action, IInterceptor, IKeyValueMap, IMapDidChange, IMapWillChange, intercept, Lambda, observable, ObservableMap, observe, values, IObservableMapInitialValues } from "mobx" import { ComplexType, createObjectNode, escapeJsonPath, MstError, flattenTypeErrors, getContextForPath, getStateTreeNode, IAnyStateTreeNode, IAnyType, IChildNodesMap, IValidationContext, IJsonPatch, isMutable, isPlainObject, isStateTreeNode, isType, IType, IValidationResult, ModelType, ObjectNode, typecheckInternal, typeCheckFailure, TypeFlags, EMPTY_OBJECT, normalizeIdentifier, AnyObjectNode, AnyNode, IAnyModelType, asArray, cannotDetermineSubtype, getSnapshot, isValidIdentifier, ExtractCSTWithSTN, devMode, createActionInvoker, addHiddenFinalProp, addHiddenWritableProp, IHooksGetter } from "../../internal" /** @hidden */ export interface IMapType extends IType< IKeyValueMap | undefined, IKeyValueMap, IMSTMap > { hooks(hooks: IHooksGetter>): IMapType } /** @hidden */ export interface IMSTMap { // bases on ObservableMap, but fine tuned to the auto snapshot conversion of MST clear(): void delete(key: string): boolean forEach( callbackfn: (value: IT["Type"], key: string | number, map: this) => void, thisArg?: any ): void get(key: string | number): IT["Type"] | undefined has(key: string | number): boolean set(key: string | number, value: ExtractCSTWithSTN): this readonly size: number put(value: ExtractCSTWithSTN): IT["Type"] keys(): IterableIterator values(): IterableIterator entries(): IterableIterator<[string, IT["Type"]]> [Symbol.iterator](): IterableIterator<[string, IT["Type"]]> /** Merge another object into this map, returns self. */ merge( other: | IMSTMap> | IKeyValueMap> | any ): this replace( values: | IMSTMap> | IKeyValueMap> | any ): this toJSON(): IKeyValueMap toString(): string [Symbol.toStringTag]: "Map" /** * Observes this object. Triggers for the events 'add', 'update' and 'delete'. * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe * for callback details */ observe( listener: (changes: IMapDidChange) => void, fireImmediately?: boolean ): Lambda intercept(handler: IInterceptor>): Lambda } const needsIdentifierError = `Map.put can only be used to store complex values that have an identifier type attribute` function tryCollectModelTypes(type: IAnyType, modelTypes: Array): boolean { const subtypes = type.getSubTypes() if (subtypes === cannotDetermineSubtype) { return false } if (subtypes) { const subtypesArray = asArray(subtypes) for (const subtype of subtypesArray) { if (!tryCollectModelTypes(subtype, modelTypes)) return false } } if (type instanceof ModelType) { modelTypes.push(type) } return true } /** * @internal * @hidden */ export enum MapIdentifierMode { UNKNOWN, YES, NO } class MSTMap extends ObservableMap { constructor(initialData?: IObservableMapInitialValues | undefined, name?: string) { super(initialData, (observable.ref as any).enhancer, name) } get(key: string): IT["Type"] | undefined { // maybe this is over-enthousiastic? normalize numeric keys to strings return super.get("" + key) } has(key: string) { return super.has("" + key) } delete(key: string) { return super.delete("" + key) } set(key: string, value: ExtractCSTWithSTN): this { return super.set("" + key, value) } put(value: ExtractCSTWithSTN): IT["Type"] { if (!value) throw new MstError(`Map.put cannot be used to set empty values`) if (isStateTreeNode(value)) { const node = getStateTreeNode(value) if (devMode()) { if (!node.identifierAttribute) { throw new MstError(needsIdentifierError) } } if (node.identifier === null) { throw new MstError(needsIdentifierError) } this.set(node.identifier, value) return value as any } else if (!isMutable(value)) { throw new MstError(`Map.put can only be used to store complex values`) } else { const mapNode = getStateTreeNode(this as IAnyStateTreeNode) const mapType = mapNode.type as MapType if (mapType.identifierMode !== MapIdentifierMode.YES) { throw new MstError(needsIdentifierError) } const idAttr = mapType.mapIdentifierAttribute! const id = (value as any)[idAttr] if (!isValidIdentifier(id)) { // try again but this time after creating a node for the value // since it might be an optional identifier const newNode = this.put(mapType.getChildType().create(value, mapNode.environment)) return this.put(getSnapshot(newNode)) } const key = normalizeIdentifier(id) this.set(key, value) return this.get(key) as any } } } /** * @internal * @hidden */ export class MapType extends ComplexType< IKeyValueMap | undefined, IKeyValueMap, IMSTMap > { identifierMode: MapIdentifierMode = MapIdentifierMode.UNKNOWN mapIdentifierAttribute: string | undefined = undefined readonly flags = TypeFlags.Map private readonly hookInitializers: Array>> = [] constructor( name: string, private readonly _subType: IAnyType, hookInitializers: Array>> = [] ) { super(name) this._determineIdentifierMode() this.hookInitializers = hookInitializers } hooks(hooks: IHooksGetter>) { const hookInitializers = this.hookInitializers.length > 0 ? this.hookInitializers.concat(hooks) : [hooks] return new MapType(this.name, this._subType, hookInitializers) } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { this._determineIdentifierMode() return createObjectNode(this, parent, subpath, environment, initialValue) } private _determineIdentifierMode() { if (this.identifierMode !== MapIdentifierMode.UNKNOWN) { return } const modelTypes: IAnyModelType[] = [] if (tryCollectModelTypes(this._subType, modelTypes)) { const identifierAttribute: string | undefined = modelTypes.reduce( (current: IAnyModelType["identifierAttribute"], type) => { if (!type.identifierAttribute) return current if (current && current !== type.identifierAttribute) { throw new MstError( `The objects in a map should all have the same identifier attribute, expected '${current}', but child of type '${type.name}' declared attribute '${type.identifierAttribute}' as identifier` ) } return type.identifierAttribute }, undefined as IAnyModelType["identifierAttribute"] ) if (identifierAttribute) { this.identifierMode = MapIdentifierMode.YES this.mapIdentifierAttribute = identifierAttribute } else { this.identifierMode = MapIdentifierMode.NO } } } initializeChildNodes(objNode: this["N"], initialSnapshot: this["C"] = {}): IChildNodesMap { const subType = (objNode.type as this)._subType const result: IChildNodesMap = {} Object.keys(initialSnapshot!).forEach(name => { result[name] = subType.instantiate(objNode, name, undefined, initialSnapshot[name]) }) return result } createNewInstance(childNodes: IChildNodesMap): this["T"] { return new MSTMap(childNodes, this.name) as any } finalizeNewInstance(node: this["N"], instance: ObservableMap): void { _interceptReads(instance, node.unbox) const type = node.type as this type.hookInitializers.forEach(initializer => { const hooks = initializer(instance as unknown as IMSTMap) Object.keys(hooks).forEach(name => { const hook = hooks[name as keyof typeof hooks]! const actionInvoker = createActionInvoker(instance as IAnyStateTreeNode, name, hook) ;(!devMode() ? addHiddenFinalProp : addHiddenWritableProp)( instance, name, actionInvoker ) }) }) intercept(instance, this.willChange) observe(instance, this.didChange) } describe() { return this.name } getChildren(node: this["N"]): ReadonlyArray { // return (node.storedValue as ObservableMap).values() return values(node.storedValue) } getChildNode(node: this["N"], key: string): AnyNode { const childNode = node.storedValue.get("" + key) if (!childNode) throw new MstError("Not a child " + key) return childNode } willChange(change: IMapWillChange): IMapWillChange | null { const node = getStateTreeNode(change.object as IAnyStateTreeNode) const key = change.name node.assertWritable({ subpath: key }) const mapType = node.type as this const subType = mapType._subType switch (change.type) { case "update": { const { newValue } = change const oldValue = change.object.get(key) if (newValue === oldValue) return null typecheckInternal(subType, newValue) change.newValue = subType.reconcile( node.getChildNode(key), change.newValue, node, key ) mapType.processIdentifier(key, change.newValue) } break case "add": { typecheckInternal(subType, change.newValue) change.newValue = subType.instantiate(node, key, undefined, change.newValue) mapType.processIdentifier(key, change.newValue) } break } return change } private processIdentifier(expected: string, node: AnyNode): void { if (this.identifierMode === MapIdentifierMode.YES && node instanceof ObjectNode) { const identifier = node.identifier! if (identifier !== expected) throw new MstError( `A map of objects containing an identifier should always store the object under their own identifier. Trying to store key '${identifier}', but expected: '${expected}'` ) } } getSnapshot(node: this["N"]): this["S"] { const res: any = {} node.getChildren().forEach(childNode => { res[childNode.subpath] = childNode.snapshot }) return res } processInitialSnapshot(childNodes: IChildNodesMap): this["S"] { const processed: any = {} Object.keys(childNodes).forEach(key => { processed[key] = childNodes[key].getSnapshot() }) return processed } didChange(change: IMapDidChange): void { const node = getStateTreeNode(change.object as IAnyStateTreeNode) switch (change.type) { case "update": return void node.emitPatch( { op: "replace", path: escapeJsonPath(change.name), value: change.newValue.snapshot, oldValue: change.oldValue ? change.oldValue.snapshot : undefined }, node ) case "add": return void node.emitPatch( { op: "add", path: escapeJsonPath(change.name), value: change.newValue.snapshot, oldValue: undefined }, node ) case "delete": // a node got deleted, get the old snapshot and make the node die const oldSnapshot = change.oldValue.snapshot change.oldValue.die() // emit the patch return void node.emitPatch( { op: "remove", path: escapeJsonPath(change.name), oldValue: oldSnapshot }, node ) } } applyPatchLocally(node: this["N"], subpath: string, patch: IJsonPatch): void { const target = node.storedValue switch (patch.op) { case "add": case "replace": target.set(subpath, patch.value) break case "remove": target.delete(subpath) break } } applySnapshot(node: this["N"], snapshot: this["C"]): void { typecheckInternal(this, snapshot) const target = node.storedValue const currentKeys: { [key: string]: boolean } = {} Array.from(target.keys()).forEach(key => { currentKeys[key] = false }) if (snapshot) { // Don't use target.replace, as it will throw away all existing items first for (let key in snapshot) { target.set(key, snapshot[key]) currentKeys["" + key] = true } } Object.keys(currentKeys).forEach(key => { if (currentKeys[key] === false) target.delete(key) }) } getChildType(): IAnyType { return this._subType } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (!isPlainObject(value)) { return typeCheckFailure(context, value, "Value is not a plain object") } return flattenTypeErrors( Object.keys(value).map(path => this._subType.validate(value[path], getContextForPath(context, path, this._subType)) ) ) } getDefaultSnapshot(): this["C"] { return EMPTY_OBJECT as this["C"] } removeChild(node: this["N"], subpath: string) { node.storedValue.delete(subpath) } } MapType.prototype.applySnapshot = action(MapType.prototype.applySnapshot) /** * `types.map` - Creates a key based collection type who's children are all of a uniform declared type. * If the type stored in a map has an identifier, it is mandatory to store the child under that identifier in the map. * * This type will always produce [observable maps](https://mobx.js.org/api.html#observablemap) * * Example: * ```ts * const Todo = types.model({ * id: types.identifier, * task: types.string * }) * * const TodoStore = types.model({ * todos: types.map(Todo) * }) * * const s = TodoStore.create({ todos: {} }) * unprotect(s) * s.todos.set(17, { task: "Grab coffee", id: 17 }) * s.todos.put({ task: "Grab cookie", id: 18 }) // put will infer key from the identifier * console.log(s.todos.get(17).task) // prints: "Grab coffee" * ``` * * @param subtype * @returns */ export function map(subtype: IT): IMapType { return new MapType(`Map`, subtype) } /** * Returns if a given value represents a map type. * * @param type * @returns `true` if it is a map type. */ export function isMapType(type: unknown): type is IMapType { return isType(type) && (type.flags & TypeFlags.Map) > 0 } ================================================ FILE: src/types/complex-types/model.ts ================================================ import { IObjectDidChange, IObjectWillChange, _getAdministration, _interceptReads, action, computed, defineProperty, getAtom, intercept, makeObservable, observable, observe, set } from "mobx" import { AnyNode, AnyObjectNode, ArrayType, ComplexType, EMPTY_ARRAY, EMPTY_OBJECT, FunctionWithFlag, Hook, IAnyType, IChildNodesMap, IJsonPatch, IType, IValidationContext, IValidationResult, Instance, MapType, MstError, TypeFlags, _CustomOrOther, _NotCustomized, addHiddenFinalProp, addHiddenWritableProp, assertArg, assertIsString, createActionInvoker, createObjectNode, devMode, escapeJsonPath, flattenTypeErrors, freeze, getContextForPath, getPrimitiveFactoryFromValue, getStateTreeNode, isPlainObject, isPrimitive, isStateTreeNode, isType, mobxShallow, optional, typeCheckFailure, typecheckInternal } from "../../internal" const PRE_PROCESS_SNAPSHOT = "preProcessSnapshot" const POST_PROCESS_SNAPSHOT = "postProcessSnapshot" /** @hidden */ export interface ModelProperties { [key: string]: IAnyType } /** @hidden */ export type ModelPrimitive = string | number | boolean | Date | bigint /** @hidden */ export interface ModelPropertiesDeclaration { [key: string]: ModelPrimitive | IAnyType } /** * Unmaps syntax property declarations to a map of { propName: IType } * * @hidden */ export type ModelPropertiesDeclarationToProperties = T extends { [k: string]: IAnyType } // optimization to reduce nesting ? T : { [K in keyof T]: T[K] extends IAnyType // keep IAnyType check on the top to reduce nesting ? T[K] : T[K] extends string ? IType : T[K] extends number ? IType : T[K] extends boolean ? IType : T[K] extends Date ? IType : never } /** * Checks if a value is optional (undefined, any or unknown). * @hidden * * Examples: * - string = false * - undefined = true * - string | undefined = true * - string & undefined = false, but we don't care * - any = true * - unknown = true */ type IsOptionalValue = undefined extends C ? TV : FV // type _A = IsOptionalValue // false // type _B = IsOptionalValue // true // type _C = IsOptionalValue // true // type _D = IsOptionalValue // false, but we don't care // type _E = IsOptionalValue // true // type _F = IsOptionalValue // true type AnyObject = Record /** * Name of the properties of an object that can't be set to undefined, any or unknown * @hidden */ type DefinablePropsNames = { [K in keyof T]: IsOptionalValue }[keyof T] /** @hidden */ export type ExtractCFromProps

= MaybeEmpty<{ [k in keyof P]: P[k]["CreationType"] | P[k]["TypeWithoutSTN"] }> /** @hidden */ export type MaybeEmpty = keyof T extends never ? EmptyObject : T /** @hidden */ export type ModelCreationType = MaybeEmpty<{ [P in DefinablePropsNames]: PC[P] }> & Partial // Ensure an object can be given additional properties. // // For the empty object type, `Record`, we need to convert into a record type that will allow us to // assign new values. Any other type should allow additional properties by default. type WithAdditionalProperties = T extends Record ? EmptyObject : T declare const $nonEmptyObject: unique symbol type EmptyObject = { [$nonEmptyObject]?: never } /** @hidden */ export type ModelCreationType2

= MaybeEmpty< keyof P extends never ? _CustomOrOther> : _CustomOrOther>> > /** @hidden */ export type ModelSnapshotType

= { [K in keyof P]: P[K]["SnapshotType"] } /** @hidden */ export type ModelSnapshotType2

= _CustomOrOther< CustomS, ModelSnapshotType

> /** * @hidden * we keep this separate from ModelInstanceType to shorten model instance types generated declarations */ export type ModelInstanceTypeProps

= { [K in keyof P]: P[K]["Type"] } /** * @hidden * do not transform this to an interface or model instance type generated declarations will be longer */ export type ModelInstanceType

= ModelInstanceTypeProps

& O /** @hidden */ export interface ModelActions { [key: string]: FunctionWithFlag } export interface IModelType< PROPS extends ModelProperties, OTHERS, CustomC = _NotCustomized, CustomS = _NotCustomized > extends IType< ModelCreationType2, ModelSnapshotType2, ModelInstanceType > { readonly properties: PROPS named(newName: string): IModelType // warning: redefining props after a process snapshot is used ends up on the fixed (custom) C, S typings being overridden // so it is recommended to use pre/post process snapshot after all props have been defined props( props: PROPS2 ): IModelType, OTHERS, CustomC, CustomS> views( fn: (self: Instance) => V ): IModelType actions( fn: (self: Instance) => A ): IModelType volatile( fn: (self: Instance) => TP ): IModelType extend( fn: (self: Instance) => { actions?: A; views?: V; state?: VS } ): IModelType preProcessSnapshot>( fn: (snapshot: NewC) => WithAdditionalProperties> ): IModelType postProcessSnapshot>( fn: (snapshot: ModelSnapshotType2) => NewS ): IModelType } /** * Any model type. */ export interface IAnyModelType extends IModelType {} /** @hidden */ export type ExtractProps = T extends IModelType ? P : never /** @hidden */ export type ExtractOthers = T extends IModelType ? O : never function objectTypeToString(this: any) { return getStateTreeNode(this).toString() } /** * @internal * @hidden */ export interface ModelTypeConfig { name?: string properties?: ModelPropertiesDeclaration initializers?: ReadonlyArray<(instance: any) => any> preProcessor?: (snapshot: any) => any postProcessor?: (snapshot: any) => any } const defaultObjectOptions = { name: "AnonymousModel", properties: {}, initializers: EMPTY_ARRAY } function toPropertiesObject(declaredProps: ModelPropertiesDeclaration): ModelProperties { const keysList = Object.keys(declaredProps) const alreadySeenKeys = new Set() keysList.forEach(key => { if (alreadySeenKeys.has(key)) { throw new MstError( `${key} is declared twice in the model. Model should not contain the same keys` ) } alreadySeenKeys.add(key) }) // loop through properties and ensures that all items are types return keysList.reduce( (props, key) => { // warn if user intended a HOOK if (key in Hook) { throw new MstError( `Hook '${key}' was defined as property. Hooks should be defined as part of the actions` ) } // the user intended to use a view const descriptor = Object.getOwnPropertyDescriptor(declaredProps, key)! if ("get" in descriptor) { throw new MstError( "Getters are not supported as properties. Please use views instead" ) } // undefined and null are not valid const value = descriptor.value if (value === null || value === undefined) { throw new MstError( "The default value of an attribute cannot be null or undefined as the type cannot be inferred. Did you mean `types.maybe(someType)`?" ) } // its a primitive, convert to its type else if (isPrimitive(value)) { props[key] = optional(getPrimitiveFactoryFromValue(value), value) } // map defaults to empty object automatically for models else if (value instanceof MapType) { props[key] = optional(value, {}) } else if (value instanceof ArrayType) { props[key] = optional(value, []) } // its already a type else if (isType(value)) { // do nothing, it's already a type } // its a function, maybe the user wanted a view? else if (devMode() && typeof value === "function") { throw new MstError( `Invalid type definition for property '${key}', it looks like you passed a function. Did you forget to invoke it, or did you intend to declare a view / action?` ) } // no other complex values else if (devMode() && typeof value === "object") { throw new MstError( `Invalid type definition for property '${key}', it looks like you passed an object. Try passing another model type or a types.frozen.` ) } else { throw new MstError( `Invalid type definition for property '${key}', cannot infer a type from a value like '${value}' (${typeof value})` ) } return props }, { ...declaredProps } as any ) } /** * @internal * @hidden */ export class ModelType< PROPS extends ModelProperties, OTHERS, CustomC, CustomS, MT extends IModelType > extends ComplexType< ModelCreationType2, ModelSnapshotType2, ModelInstanceType > implements IModelType { readonly flags = TypeFlags.Object /* * The original object definition */ public readonly initializers!: ((instance: any) => any)[] public readonly properties!: PROPS private preProcessor!: (snapshot: any) => any | undefined private postProcessor!: (snapshot: any) => any | undefined private readonly propertyNames: string[] constructor(opts: ModelTypeConfig) { super(opts.name || defaultObjectOptions.name) Object.assign(this, defaultObjectOptions, opts) // ensures that any default value gets converted to its related type this.properties = toPropertiesObject(this.properties) as PROPS freeze(this.properties) // make sure nobody messes with it this.propertyNames = Object.keys(this.properties) this.identifierAttribute = this._getIdentifierAttribute() } private _getIdentifierAttribute(): string | undefined { let identifierAttribute: string | undefined = undefined this.forAllProps((propName, propType) => { if (propType.flags & TypeFlags.Identifier) { if (identifierAttribute) throw new MstError( `Cannot define property '${propName}' as object identifier, property '${identifierAttribute}' is already defined as identifier property` ) identifierAttribute = propName } }) return identifierAttribute } cloneAndEnhance(opts: ModelTypeConfig): IAnyModelType { return new ModelType({ name: opts.name || this.name, properties: Object.assign({}, this.properties, opts.properties), initializers: this.initializers.concat(opts.initializers || []), preProcessor: opts.preProcessor || this.preProcessor, postProcessor: opts.postProcessor || this.postProcessor }) } actions(fn: (self: Instance) => A) { const actionInitializer = (self: Instance) => { this.instantiateActions(self, fn(self)) return self } return this.cloneAndEnhance({ initializers: [actionInitializer] }) } private instantiateActions(self: this["T"], actions: ModelActions): void { // check if return is correct if (!isPlainObject(actions)) { throw new MstError( `actions initializer should return a plain object containing actions` ) } // bind actions to the object created Object.getOwnPropertyNames(actions).forEach(name => { if (name in this.properties) { throw new MstError(`'${name}' is a property and cannot be declared as an action`) } // warn if preprocessor was given if (name === PRE_PROCESS_SNAPSHOT) throw new MstError( `Cannot define action '${PRE_PROCESS_SNAPSHOT}', it should be defined using 'type.preProcessSnapshot(fn)' instead` ) // warn if postprocessor was given if (name === POST_PROCESS_SNAPSHOT) throw new MstError( `Cannot define action '${POST_PROCESS_SNAPSHOT}', it should be defined using 'type.postProcessSnapshot(fn)' instead` ) let action2 = actions[name] // apply hook composition let baseAction = (self as any)[name] if (name in Hook && baseAction) { let specializedAction = action2 action2 = function () { baseAction.apply(null, arguments) specializedAction.apply(null, arguments) } } // the goal of this is to make sure actions using "this" can call themselves, // while still allowing the middlewares to register them const middlewares = (action2 as any).$mst_middleware // make sure middlewares are not lost let boundAction = action2.bind(actions) boundAction._isFlowAction = (action2 as FunctionWithFlag)._isFlowAction || false boundAction.$mst_middleware = middlewares const actionInvoker = createActionInvoker(self as any, name, boundAction) actions[name] = actionInvoker // See #646, allow models to be mocked ;(!devMode() ? addHiddenFinalProp : addHiddenWritableProp)(self, name, actionInvoker) }) } named: MT["named"] = name => { return this.cloneAndEnhance({ name }) } props: MT["props"] = properties => { return this.cloneAndEnhance({ properties }) } volatile(fn: (self: Instance) => TP) { if (typeof fn !== "function") { throw new MstError( `You passed an ${typeof fn} to volatile state as an argument, when function is expected` ) } const stateInitializer = (self: Instance) => { this.instantiateVolatileState(self, fn(self)) return self } return this.cloneAndEnhance({ initializers: [stateInitializer] }) } private instantiateVolatileState( self: this["T"], state: { [key: string]: any } ): void { // check views return if (!isPlainObject(state)) { throw new MstError( `volatile state initializer should return a plain object containing state` ) } Object.getOwnPropertyNames(state).forEach(name => { if (name in this.properties) { throw new MstError( `'${name}' is a property and cannot be declared as volatile state` ) } set(self, name, state[name]) }) } extend( fn: (self: Instance) => { actions?: A; views?: V; state?: VS } ) { const initializer = (self: Instance) => { const { actions, views, state, ...rest } = fn(self) for (let key in rest) throw new MstError( `The \`extend\` function should return an object with a subset of the fields 'actions', 'views' and 'state'. Found invalid key '${key}'` ) if (state) this.instantiateVolatileState(self, state) if (views) this.instantiateViews(self, views) if (actions) this.instantiateActions(self, actions) return self } return this.cloneAndEnhance({ initializers: [initializer] }) } views(fn: (self: Instance) => V) { const viewInitializer = (self: Instance) => { this.instantiateViews(self, fn(self)) return self } return this.cloneAndEnhance({ initializers: [viewInitializer] }) } private instantiateViews(self: this["T"], views: Object): void { // check views return if (!isPlainObject(views)) { throw new MstError(`views initializer should return a plain object containing views`) } Object.getOwnPropertyNames(views).forEach(name => { if (name in this.properties) { throw new MstError(`'${name}' is a property and cannot be declared as a view`) } // is this a computed property? const descriptor = Object.getOwnPropertyDescriptor(views, name)! if ("get" in descriptor) { defineProperty(self, name, descriptor) makeObservable(self, { [name]: computed } as any) } else if (typeof descriptor.value === "function") { // this is a view function, merge as is! // See #646, allow models to be mocked ;(!devMode() ? addHiddenFinalProp : addHiddenWritableProp)( self, name, descriptor.value ) } else { throw new MstError( `A view member should either be a function or getter based property` ) } }) } preProcessSnapshot: MT["preProcessSnapshot"] = preProcessor => { const currentPreprocessor = this.preProcessor if (!currentPreprocessor) return this.cloneAndEnhance({ preProcessor }) else return this.cloneAndEnhance({ preProcessor: snapshot => currentPreprocessor(preProcessor(snapshot)) }) } postProcessSnapshot: MT["postProcessSnapshot"] = postProcessor => { const currentPostprocessor = this.postProcessor if (!currentPostprocessor) return this.cloneAndEnhance({ postProcessor }) else return this.cloneAndEnhance({ postProcessor: snapshot => postProcessor(currentPostprocessor(snapshot)) }) } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { const value = isStateTreeNode(initialValue) ? initialValue : this.applySnapshotPreProcessor(initialValue) return createObjectNode(this, parent, subpath, environment, value) // Optimization: record all prop- view- and action names after first construction, and generate an optimal base class // that pre-reserves all these fields for fast object-member lookups } initializeChildNodes(objNode: this["N"], initialSnapshot: any = {}): IChildNodesMap { const type = objNode.type as this const result: IChildNodesMap = {} type.forAllProps((name, childType) => { result[name] = childType.instantiate( objNode, name, undefined, (initialSnapshot as any)[name] ) }) return result } createNewInstance(childNodes: IChildNodesMap): this["T"] { const options = { ...mobxShallow, name: this.name } return observable.object(childNodes, EMPTY_OBJECT, options) as any } finalizeNewInstance(node: this["N"], instance: this["T"]): void { addHiddenFinalProp(instance, "toString", objectTypeToString) this.forAllProps(name => { _interceptReads(instance, name, node.unbox) }) this.initializers.reduce((self, fn) => fn(self), instance) intercept(instance, this.willChange) observe(instance, this.didChange) } private willChange(chg: IObjectWillChange): IObjectWillChange | null { // TODO: mobx typings don't seem to take into account that newValue can be set even when removing a prop const change = chg as IObjectWillChange & { newValue?: any } const node = getStateTreeNode(change.object) const subpath = change.name as string node.assertWritable({ subpath }) const childType = (node.type as this).properties[subpath] // only properties are typed, state are stored as-is references if (childType) { typecheckInternal(childType, change.newValue) change.newValue = childType.reconcile( node.getChildNode(subpath), change.newValue, node, subpath ) } return change } private didChange(chg: IObjectDidChange) { // TODO: mobx typings don't seem to take into account that newValue can be set even when removing a prop const change = chg as IObjectWillChange & { newValue?: any; oldValue?: any } const childNode = getStateTreeNode(change.object) const childType = (childNode.type as this).properties[change.name as string] if (!childType) { // don't emit patches for volatile state return } const oldChildValue = change.oldValue ? change.oldValue.snapshot : undefined childNode.emitPatch( { op: "replace", path: escapeJsonPath(change.name as string), value: change.newValue.snapshot, oldValue: oldChildValue }, childNode ) } getChildren(node: this["N"]): ReadonlyArray { const res: AnyNode[] = [] this.forAllProps(name => { res.push(this.getChildNode(node, name)) }) return res } getChildNode(node: this["N"], key: string): AnyNode { if (!(key in this.properties)) throw new MstError("Not a value property: " + key) const adm = _getAdministration(node.storedValue, key) const childNode = adm.raw?.() if (!childNode) throw new MstError("Node not available for property " + key) return childNode } getSnapshot(node: this["N"], applyPostProcess = true): this["S"] { const res = {} as any this.forAllProps((name, type) => { // TODO: FIXME, make sure the observable ref is used! const atom = getAtom(node.storedValue, name) ;(atom as any).reportObserved() res[name] = this.getChildNode(node, name).snapshot }) if (applyPostProcess) { return this.applySnapshotPostProcessor(res) } return res } processInitialSnapshot(childNodes: IChildNodesMap): this["S"] { const processed = {} as any Object.keys(childNodes).forEach(key => { processed[key] = childNodes[key].getSnapshot() }) return this.applySnapshotPostProcessor(processed) } applyPatchLocally(node: this["N"], subpath: string, patch: IJsonPatch): void { if (!(patch.op === "replace" || patch.op === "add")) { throw new MstError(`object does not support operation ${patch.op}`) } ;(node.storedValue as any)[subpath] = patch.value } applySnapshot(node: this["N"], snapshot: this["C"]): void { typecheckInternal(this, snapshot) const preProcessedSnapshot = this.applySnapshotPreProcessor(snapshot) this.forAllProps(name => { ;(node.storedValue as any)[name] = preProcessedSnapshot[name] }) } applySnapshotPreProcessor(snapshot: any) { const processor = this.preProcessor return processor ? processor.call(null, snapshot) : snapshot } applySnapshotPostProcessor(snapshot: any) { const postProcessor = this.postProcessor if (postProcessor) return postProcessor.call(null, snapshot) return snapshot } getChildType(propertyName: string): IAnyType { assertIsString(propertyName, 1) return this.properties[propertyName] } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { let snapshot = this.applySnapshotPreProcessor(value) if (!isPlainObject(snapshot)) { return typeCheckFailure(context, snapshot, "Value is not a plain object") } return flattenTypeErrors( this.propertyNames.map(key => this.properties[key].validate( snapshot[key], getContextForPath(context, key, this.properties[key]) ) ) ) } private forAllProps(fn: (name: string, type: IAnyType) => void) { this.propertyNames.forEach(key => fn(key, this.properties[key])) } describe() { // optimization: cache return ( "{ " + this.propertyNames.map(key => key + ": " + this.properties[key].describe()).join("; ") + " }" ) } getDefaultSnapshot(): this["C"] { return EMPTY_OBJECT as this["C"] } removeChild(node: this["N"], subpath: string) { ;(node.storedValue as any)[subpath] = undefined } } ModelType.prototype.applySnapshot = action(ModelType.prototype.applySnapshot) export function model

( name: string, properties?: P ): IModelType, {}> export function model

( properties?: P ): IModelType, {}> /** * `types.model` - Creates a new model type by providing a name, properties, volatile state and actions. * * See the [model type](/concepts/trees#creating-models) description or the [getting started](intro/getting-started.md#getting-started-1) tutorial. */ export function model(...args: any[]): any { if (devMode() && typeof args[0] !== "string" && args[1]) { throw new MstError( "Model creation failed. First argument must be a string when two arguments are provided" ) } const name = typeof args[0] === "string" ? args.shift() : "AnonymousModel" const properties = args.shift() || {} return new ModelType({ name, properties }) } // TODO: this can be simplified in TS3, since we can transform _NotCustomized to unknown, since unkonwn & X = X // and then back unknown to _NotCustomized if needed /** @hidden */ export type _CustomJoin = A extends _NotCustomized ? B : A & B // generated with C:\VSProjects\github\mobx-state-tree-upstream\packages\mobx-state-tree\scripts\generate-compose-type.js // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType): IModelType, _CustomJoin> // prettier-ignore export function compose(A: IModelType, B: IModelType): IModelType, _CustomJoin> // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType, C: IModelType): IModelType>, _CustomJoin>> // prettier-ignore export function compose(A: IModelType, B: IModelType, C: IModelType): IModelType>, _CustomJoin>> // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType, C: IModelType, D: IModelType): IModelType>>, _CustomJoin>>> // prettier-ignore export function compose(A: IModelType, B: IModelType, C: IModelType, D: IModelType): IModelType>>, _CustomJoin>>> // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType): IModelType>>>, _CustomJoin>>>> // prettier-ignore export function compose(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType): IModelType>>>, _CustomJoin>>>> // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType): IModelType>>>>, _CustomJoin>>>>> // prettier-ignore export function compose(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType): IModelType>>>>, _CustomJoin>>>>> // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType): IModelType>>>>>, _CustomJoin>>>>>> // prettier-ignore export function compose(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType): IModelType>>>>>, _CustomJoin>>>>>> // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType): IModelType>>>>>>, _CustomJoin>>>>>>> // prettier-ignore export function compose(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType): IModelType>>>>>>, _CustomJoin>>>>>>> // prettier-ignore export function compose(name: string, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType, I: IModelType): IModelType>>>>>>>, _CustomJoin>>>>>>>> // prettier-ignore export function compose(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType, I: IModelType): IModelType>>>>>>>, _CustomJoin>>>>>>>> /** * `types.compose` - Composes a new model from one or more existing model types. * This method can be invoked in two forms: * Given 2 or more model types, the types are composed into a new Type. * Given first parameter as a string and 2 or more model types, * the types are composed into a new Type with the given name */ export function compose(...args: any[]): any { // TODO: just join the base type names if no name is provided const hasTypename = typeof args[0] === "string" const typeName: string = hasTypename ? args[0] : "AnonymousModel" if (hasTypename) { args.shift() } // check all parameters if (devMode()) { args.forEach((type, i) => { assertArg(type, isModelType, "mobx-state-tree model type", hasTypename ? i + 2 : i + 1) }) } return args .reduce((prev, cur) => prev.cloneAndEnhance({ name: prev.name + "_" + cur.name, properties: cur.properties, initializers: cur.initializers, preProcessor: (snapshot: any) => cur.applySnapshotPreProcessor(prev.applySnapshotPreProcessor(snapshot)), postProcessor: (snapshot: any) => cur.applySnapshotPostProcessor(prev.applySnapshotPostProcessor(snapshot)) }) ) .named(typeName) } /** * Returns if a given value represents a model type. * * @param type * @returns */ export function isModelType(type: unknown): type is IAnyModelType { return isType(type) && (type.flags & TypeFlags.Object) > 0 } ================================================ FILE: src/types/index.ts ================================================ // we import the types to re-export them inside types. import { enumeration, model, compose, custom, reference, safeReference, union, optional, literal, maybe, maybeNull, refinement, string, boolean, number, integer, float, finite, bigint, DatePrimitive, map, array, frozen, identifier, identifierNumber, late, lazy, undefinedType, nullType, snapshotProcessor } from "../internal" export const types = { enumeration, model, compose, custom, reference, safeReference, union, optional, literal, maybe, maybeNull, refinement, string, boolean, number, integer, float, finite, bigint, Date: DatePrimitive, map, array, frozen, identifier, identifierNumber, late, lazy, undefined: undefinedType, null: nullType, snapshotProcessor } ================================================ FILE: src/types/primitives.ts ================================================ import { SimpleType, isPrimitive, MstError, identity, createScalarNode, ISimpleType, IType, TypeFlags, IValidationContext, IValidationResult, typeCheckSuccess, typeCheckFailure, isType, isInteger, AnyObjectNode, AnyNode, isFloat, isFinite } from "../internal" // TODO: implement CoreType using types.custom ? /** * @internal * @hidden */ export class CoreType extends SimpleType { constructor( name: string, readonly flags: TypeFlags, private readonly checker: (value: C) => boolean, private readonly initializer: (v: C) => T = identity ) { super(name) this.flags = flags } describe() { return this.name } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: C ): this["N"] { return createScalarNode(this, parent, subpath, environment, initialValue) } createNewInstance(snapshot: C) { return this.initializer(snapshot) } isValidSnapshot(value: C, context: IValidationContext): IValidationResult { if (isPrimitive(value) && this.checker(value as any)) { return typeCheckSuccess() } const typeName = this.name === "Date" ? "Date or a unix milliseconds timestamp" : this.name return typeCheckFailure(context, value, `Value is not a ${typeName}`) } } /** * `types.string` - Creates a type that can only contain a string value. * This type is used for string values by default * * Example: * ```ts * const Person = types.model({ * firstName: types.string, * lastName: "Doe" * }) * ``` */ // tslint:disable-next-line:variable-name export const string: ISimpleType = new CoreType( "string", TypeFlags.String, v => typeof v === "string" ) /** * `types.number` - Creates a type that can only contain a numeric value. * This type is used for numeric values by default * * Example: * ```ts * const Vector = types.model({ * x: types.number, * y: 1.5 * }) * ``` */ // tslint:disable-next-line:variable-name export const number: ISimpleType = new CoreType( "number", TypeFlags.Number, v => typeof v === "number" ) /** * `types.integer` - Creates a type that can only contain an integer value. * * Example: * ```ts * const Size = types.model({ * width: types.integer, * height: 10 * }) * ``` */ // tslint:disable-next-line:variable-name export const integer: ISimpleType = new CoreType( "integer", TypeFlags.Integer, v => isInteger(v) ) /** * `types.float` - Creates a type that can only contain an float value. * * Example: * ```ts * const Size = types.model({ * width: types.float, * height: 10 * }) * ``` */ // tslint:disable-next-line:variable-name export const float: ISimpleType = new CoreType( "float", TypeFlags.Float, v => isFloat(v) ) /** * `types.finite` - Creates a type that can only contain an finite value. * * Example: * ```ts * const Size = types.model({ * width: types.finite, * height: 10 * }) * ``` */ // tslint:disable-next-line:variable-name export const finite: ISimpleType = new CoreType( "finite", TypeFlags.Finite, v => isFinite(v) ) const _BigIntPrimitive = new CoreType( "bigint", TypeFlags.BigInt, v => { if (typeof v === "bigint") { return true } if (typeof v === "string" || typeof v === "number") { try { // BigInt primitive constructor verifies whether the value is a valid integer BigInt(v) return true } catch {} } return false }, v => (typeof v === "bigint" ? v : BigInt(v)) ) _BigIntPrimitive.getSnapshot = function (node: AnyNode) { return String(node.storedValue) } /** * `types.bigint` - Creates a type that can only contain a bigint value. * Snapshots serialize to string (JSON-safe) and deserialize from string, number or bigint. * * Example: * ```ts * const BigId = types.model({ * id: types.identifier, * value: types.bigint * }) * getSnapshot(store).value // "0" (string, JSON-safe) * ``` */ export const bigint: IType = _BigIntPrimitive /** * `types.boolean` - Creates a type that can only contain a boolean value. * This type is used for boolean values by default * * Example: * ```ts * const Thing = types.model({ * isCool: types.boolean, * isAwesome: false * }) * ``` */ // tslint:disable-next-line:variable-name export const boolean: ISimpleType = new CoreType( "boolean", TypeFlags.Boolean, v => typeof v === "boolean" ) /** * `types.null` - The type of the value `null` */ export const nullType: ISimpleType = new CoreType( "null", TypeFlags.Null, v => v === null ) /** * `types.undefined` - The type of the value `undefined` */ export const undefinedType: ISimpleType = new CoreType( "undefined", TypeFlags.Undefined, v => v === undefined ) const _DatePrimitive = new CoreType( "Date", TypeFlags.Date, v => typeof v === "number" || v instanceof Date, v => (v instanceof Date ? v : new Date(v)) ) _DatePrimitive.getSnapshot = function (node: AnyNode) { return node.storedValue.getTime() } /** * `types.Date` - Creates a type that can only contain a javascript Date value. * * Example: * ```ts * const LogLine = types.model({ * timestamp: types.Date, * }) * * LogLine.create({ timestamp: new Date() }) * ``` */ export const DatePrimitive: IType = _DatePrimitive /** * @internal * @hidden */ export function getPrimitiveFactoryFromValue(value: any): ISimpleType { switch (typeof value) { case "string": return string case "number": return number // In the future, isInteger(value) ? integer : number would be interesting, but would be too breaking for now case "boolean": return boolean case "bigint": return bigint case "object": if (value instanceof Date) return DatePrimitive } throw new MstError("Cannot determine primitive type from value " + value) } /** * Returns if a given value represents a primitive type. * * @param type * @returns */ export function isPrimitiveType( type: unknown ): type is | ISimpleType | ISimpleType | ISimpleType | typeof bigint | typeof DatePrimitive { return ( isType(type) && (type.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.Integer | TypeFlags.Boolean | TypeFlags.Date | TypeFlags.BigInt)) > 0 ) } ================================================ FILE: src/types/utility-types/custom.ts ================================================ import { createScalarNode, SimpleType, IType, TypeFlags, IValidationContext, IValidationResult, typeCheckSuccess, typeCheckFailure, AnyObjectNode } from "../../internal" export interface CustomTypeOptions { /** Friendly name */ name: string /** given a serialized value and environment, how to turn it into the target type */ fromSnapshot(snapshot: S, env?: any): T /** return the serialization of the current value */ toSnapshot(value: T): S /** if true, this is a converted value, if false, it's a snapshot */ isTargetType(value: T | S): boolean /** a non empty string is assumed to be a validation error */ getValidationMessage(snapshot: S): string // TODO: isSnapshotEqual // TODO: isValueEqual } /** * `types.custom` - Creates a custom type. Custom types can be used for arbitrary immutable values, that have a serializable representation. For example, to create your own Date representation, Decimal type etc. * * The signature of the options is: * ```ts * export interface CustomTypeOptions { * // Friendly name * name: string * // given a serialized value and environment, how to turn it into the target type * fromSnapshot(snapshot: S, env: any): T * // return the serialization of the current value * toSnapshot(value: T): S * // if true, this is a converted value, if false, it's a snapshot * isTargetType(value: T | S): value is T * // a non empty string is assumed to be a validation error * getValidationMessage?(snapshot: S): string * } * ``` * * Example: * ```ts * const DecimalPrimitive = types.custom({ * name: "Decimal", * fromSnapshot(value: string) { * return new Decimal(value) * }, * toSnapshot(value: Decimal) { * return value.toString() * }, * isTargetType(value: string | Decimal): boolean { * return value instanceof Decimal * }, * getValidationMessage(value: string): string { * if (/^-?\d+\.\d+$/.test(value)) return "" // OK * return `'${value}' doesn't look like a valid decimal number` * } * }) * * const Wallet = types.model({ * balance: DecimalPrimitive * }) * ``` * * @param options * @returns */ export function custom(options: CustomTypeOptions): IType { return new CustomType(options) } /** * @internal * @hidden */ export class CustomType extends SimpleType { readonly flags = TypeFlags.Custom constructor(protected readonly options: CustomTypeOptions) { super(options.name) } describe() { return this.name } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (this.options.isTargetType(value)) return typeCheckSuccess() const typeError: string = this.options.getValidationMessage(value as S) if (typeError) { return typeCheckFailure( context, value, `Invalid value for type '${this.name}': ${typeError}` ) } return typeCheckSuccess() } getSnapshot(node: this["N"]): S { return this.options.toSnapshot(node.storedValue) } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: S | T ): this["N"] { const valueToStore: T = this.options.isTargetType(initialValue) ? (initialValue as T) : this.options.fromSnapshot(initialValue as S, parent && parent.root.environment) return createScalarNode(this, parent, subpath, environment, valueToStore) } reconcile(current: this["N"], value: S | T, parent: AnyObjectNode, subpath: string): this["N"] { const isSnapshot = !this.options.isTargetType(value) // in theory customs use scalar nodes which cannot be detached, but still... if (!current.isDetaching) { const unchanged = current.type === this && (isSnapshot ? value === current.snapshot : value === current.storedValue) if (unchanged) { current.setParent(parent, subpath) return current } } const valueToStore: T = isSnapshot ? this.options.fromSnapshot(value as S, parent.root.environment) : (value as T) const newNode = this.instantiate(parent, subpath, undefined, valueToStore) current.die() // noop if detaching return newNode } } ================================================ FILE: src/types/utility-types/enumeration.ts ================================================ import { ISimpleType, union, literal, assertIsString, devMode } from "../../internal" /** @hidden */ export type UnionStringArray = T[number] // strongly typed enumeration forms for plain and readonly string arrays (when passed directly to the function). // with these overloads, we get correct typing for native TS string enums when we use Object.values(Enum) as Enum[] as options. // these overloads also allow both mutable and immutable arrays, making types.enumeration(Object.values(Enum)) possible. // the only case where this doesn't work is when passing to the function an array variable with a mutable type constraint; // for these cases, it will just fallback and assume the type is a generic string. export function enumeration( options: readonly T[] ): ISimpleType> export function enumeration( name: string, options: readonly T[] ): ISimpleType> /** * `types.enumeration` - Can be used to create an string based enumeration. * (note: this methods is just sugar for a union of string literals) * * Example: * ```ts * const TrafficLight = types.model({ * color: types.enumeration("Color", ["Red", "Orange", "Green"]) * }) * ``` * * @param name descriptive name of the enumeration (optional) * @param options possible values this enumeration can have * @returns */ export function enumeration( name: string | readonly T[], options?: readonly T[] ): ISimpleType { const realOptions: readonly T[] = typeof name === "string" ? options! : name // check all options if (devMode()) { realOptions.forEach((option, i) => { assertIsString(option, i + 1) }) } const type = union(...realOptions.map(option => literal("" + option))) if (typeof name === "string") type.name = name return type as ISimpleType } ================================================ FILE: src/types/utility-types/frozen.ts ================================================ import { isSerializable, deepFreeze, createScalarNode, IValidationContext, IValidationResult, typeCheckSuccess, typeCheckFailure, TypeFlags, isType, optional, IType, IAnyType, AnyObjectNode, SimpleType, type ISimpleType } from "../../internal" /** * @internal * @hidden */ export class Frozen extends SimpleType { flags = TypeFlags.Frozen constructor(private subType?: IAnyType) { super(subType ? `frozen(${subType.name})` : "frozen") } describe() { return "" } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, value: this["C"] ): this["N"] { // create the node return createScalarNode(this, parent, subpath, environment, deepFreeze(value)) } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (!isSerializable(value)) { return typeCheckFailure( context, value, "Value is not serializable and cannot be frozen" ) } if (this.subType) return this.subType.validate(value, context) return typeCheckSuccess() } } const untypedFrozenInstance = new Frozen() export function frozen(subType: IType): IType export function frozen(defaultValue: T): IType export function frozen(): IType // do not assume undefined by default, let the user specify it if needed /** * `types.frozen` - Frozen can be used to store any value that is serializable in itself (that is valid JSON). * Frozen values need to be immutable or treated as if immutable. They need be serializable as well. * Values stored in frozen will snapshotted as-is by MST, and internal changes will not be tracked. * * This is useful to store complex, but immutable values like vectors etc. It can form a powerful bridge to parts of your application that should be immutable, or that assume data to be immutable. * * Note: if you want to store free-form state that is mutable, or not serializeable, consider using volatile state instead. * * Frozen properties can be defined in three different ways * 1. `types.frozen(SubType)` - provide a valid MST type and frozen will check if the provided data conforms the snapshot for that type * 2. `types.frozen({ someDefaultValue: true})` - provide a primitive value, object or array, and MST will infer the type from that object, and also make it the default value for the field * 3. `types.frozen()` - provide a typescript type, to help in strongly typing the field (design time only) * * Example: * ```ts * const GameCharacter = types.model({ * name: string, * location: types.frozen({ x: 0, y: 0}) * }) * * const hero = GameCharacter.create({ * name: "Mario", * location: { x: 7, y: 4 } * }) * * hero.location = { x: 10, y: 2 } // OK * hero.location.x = 7 // Not ok! * ``` * * ```ts * type Point = { x: number, y: number } * const Mouse = types.model({ * loc: types.frozen() * }) * ``` * * @param defaultValueOrType * @returns */ export function frozen(arg?: any): any { if (arguments.length === 0) return untypedFrozenInstance else if (isType(arg)) return new Frozen(arg) else return optional(untypedFrozenInstance, arg) } /** * Returns if a given value represents a frozen type. * * @param type * @returns */ export function isFrozenType(type: unknown): type is ISimpleType { return isType(type) && (type.flags & TypeFlags.Frozen) > 0 } ================================================ FILE: src/types/utility-types/identifier.ts ================================================ import { MstError, createScalarNode, SimpleType, TypeFlags, isType, IValidationContext, IValidationResult, typeCheckFailure, ModelType, typeCheckSuccess, ISimpleType, AnyObjectNode, ScalarNode, assertArg } from "../../internal" abstract class BaseIdentifierType extends SimpleType { readonly flags = TypeFlags.Identifier constructor( name: string, private readonly validType: "string" | "number" ) { super(name) } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] ): this["N"] { if (!parent || !(parent.type instanceof ModelType)) throw new MstError( `Identifier types can only be instantiated as direct child of a model type` ) return createScalarNode(this, parent, subpath, environment, initialValue) } reconcile(current: this["N"], newValue: this["C"], parent: AnyObjectNode, subpath: string) { // we don't consider detaching here since identifier are scalar nodes, and scalar nodes cannot be detached if (current.storedValue !== newValue) throw new MstError( `Tried to change identifier from '${current.storedValue}' to '${newValue}'. Changing identifiers is not allowed.` ) current.setParent(parent, subpath) return current } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (typeof value !== this.validType) { return typeCheckFailure( context, value, `Value is not a valid ${this.describe()}, expected a ${this.validType}` ) } return typeCheckSuccess() } } /** * @internal * @hidden */ export class IdentifierType extends BaseIdentifierType { readonly flags = TypeFlags.Identifier constructor() { super(`identifier`, "string") } describe() { return `identifier` } } /** * @internal * @hidden */ export class IdentifierNumberType extends BaseIdentifierType { constructor() { super("identifierNumber", "number") } getSnapshot(node: ScalarNode): number { return node.storedValue } describe() { return `identifierNumber` } } /** * `types.identifier` - Identifiers are used to make references, lifecycle events and reconciling works. * Inside a state tree, for each type can exist only one instance for each given identifier. * For example there couldn't be 2 instances of user with id 1. If you need more, consider using references. * Identifier can be used only as type property of a model. * This type accepts as parameter the value type of the identifier field that can be either string or number. * * Example: * ```ts * const Todo = types.model("Todo", { * id: types.identifier, * title: types.string * }) * ``` * * @returns */ export const identifier: ISimpleType = new IdentifierType() /** * `types.identifierNumber` - Similar to `types.identifier`. This one will serialize from / to a number when applying snapshots * * Example: * ```ts * const Todo = types.model("Todo", { * id: types.identifierNumber, * title: types.string * }) * ``` * * @returns */ export const identifierNumber: ISimpleType = new IdentifierNumberType() /** * Returns if a given value represents an identifier type. * * @param type * @returns */ export function isIdentifierType( type: unknown ): type is typeof identifier | typeof identifierNumber { return isType(type) && (type.flags & TypeFlags.Identifier) > 0 } /** * Valid types for identifiers. */ export type ReferenceIdentifier = string | number /** * @internal * @hidden */ export function normalizeIdentifier(id: ReferenceIdentifier): string { return "" + id } /** * @internal * @hidden */ export function isValidIdentifier(id: any): id is ReferenceIdentifier { return typeof id === "string" || typeof id === "number" } /** * @internal * @hidden */ export function assertIsValidIdentifier(id: ReferenceIdentifier, argNumber: number | number[]) { assertArg(id, isValidIdentifier, "string or number (identifier)", argNumber) } ================================================ FILE: src/types/utility-types/late.ts ================================================ import { MstError, BaseType, IValidationContext, IValidationResult, TypeFlags, isType, IAnyType, typeCheckSuccess, AnyObjectNode, ExtractNodeType, cannotDetermineSubtype, devMode } from "../../internal" class Late extends BaseType< IT["CreationType"], IT["SnapshotType"], IT["TypeWithoutSTN"], ExtractNodeType > { private _subType?: IT get flags() { return (this._subType ? this._subType.flags : 0) | TypeFlags.Late } getSubType(mustSucceed: true): IT getSubType(mustSucceed: false): IT | undefined getSubType(mustSucceed: boolean): IT | undefined { if (!this._subType) { let t = undefined try { t = this._definition() } catch (e) { if (e instanceof ReferenceError) // can happen in strict ES5 code when a definition is self refering t = undefined else throw e } if (mustSucceed && t === undefined) throw new MstError( "Late type seems to be used too early, the definition (still) returns undefined" ) if (t) { if (devMode() && !isType(t)) throw new MstError( "Failed to determine subtype, make sure types.late returns a type definition." ) this._subType = t } } return this._subType } constructor( name: string, private readonly _definition: () => IT ) { super(name) } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { return this.getSubType(true).instantiate(parent, subpath, environment, initialValue) as any } reconcile( current: this["N"], newValue: this["C"] | this["T"], parent: AnyObjectNode, subpath: string ): this["N"] { return this.getSubType(true).reconcile(current, newValue, parent, subpath) as any } describe() { const t = this.getSubType(false) return t ? t.name : "" } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { const t = this.getSubType(false) if (!t) { // See #916; the variable the definition closure is pointing to wasn't defined yet, so can't be evaluted yet here return typeCheckSuccess() } return t.validate(value, context) } isAssignableFrom(type: IAnyType) { const t = this.getSubType(false) return t ? t.isAssignableFrom(type) : false } getSubTypes() { const subtype = this.getSubType(false) return subtype ? subtype : cannotDetermineSubtype } } export function late(type: () => T): T export function late(name: string, type: () => T): T /** * `types.late` - Defines a type that gets implemented later. This is useful when you have to deal with circular dependencies. * Please notice that when defining circular dependencies TypeScript isn't smart enough to inference them. * * Example: * ```ts * // TypeScript isn't smart enough to infer self referencing types. * const Node = types.model({ * children: types.array(types.late((): IAnyModelType => Node)) // then typecast each array element to Instance * }) * ``` * * @param name The name to use for the type that will be returned. * @param type A function that returns the type that will be defined. * @returns */ export function late(nameOrType: any, maybeType?: () => IAnyType): IAnyType { const name = typeof nameOrType === "string" ? nameOrType : `late(${nameOrType.toString()})` const type = typeof nameOrType === "string" ? maybeType : nameOrType // checks that the type is actually a late type if (devMode()) { if (!(typeof type === "function" && type.length === 0)) throw new MstError( "Invalid late type, expected a function with zero arguments that returns a type, got: " + type ) } return new Late(name, type) } /** * Returns if a given value represents a late type. * * @param type * @returns */ export function isLateType(type: unknown): type is IAnyType { return isType(type) && (type.flags & TypeFlags.Late) > 0 } ================================================ FILE: src/types/utility-types/lazy.ts ================================================ import { action, IObservableArray, observable, when } from "mobx" import { AnyNode } from "../../core/node/BaseNode" import { IType } from "../../core/type/type" import { IValidationContext, IValidationResult, TypeFlags, typeCheckSuccess, AnyObjectNode, SimpleType, createScalarNode, deepFreeze, isSerializable, typeCheckFailure } from "../../internal" interface LazyOptions, U> { loadType: () => Promise shouldLoadPredicate: (parent: U) => boolean } export function lazy, U>( name: string, options: LazyOptions ): T { // TODO: fix this unknown casting to be stricter return new Lazy(name, options) as unknown as T } /** * @internal * @hidden */ export class Lazy, U> extends SimpleType { flags = TypeFlags.Lazy private loadedType: T | null = null private pendingNodeList: IObservableArray = observable.array() constructor( name: string, private readonly options: LazyOptions ) { super(name) when( () => this.pendingNodeList.length > 0 && this.pendingNodeList.some( node => node.isAlive && this.options.shouldLoadPredicate(node.parent ? node.parent.value : null) ), () => { this.options.loadType().then( action((type: T) => { this.loadedType = type this.pendingNodeList.forEach(node => { if (!node.parent) return if (!this.loadedType) return node.parent.applyPatches([ { op: "replace", path: `/${node.subpath}`, value: node.snapshot } ]) }) }) ) } ) } describe() { return `` } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, value: this["C"] ): this["N"] { if (this.loadedType) { return this.loadedType.instantiate(parent, subpath, environment, value) as this["N"] } const node = createScalarNode(this, parent, subpath, environment, deepFreeze(value)) this.pendingNodeList.push(node) when( () => !node.isAlive, () => this.pendingNodeList.splice(this.pendingNodeList.indexOf(node), 1) ) return node } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (this.loadedType) { return this.loadedType.validate(value, context) } if (!isSerializable(value)) { return typeCheckFailure(context, value, "Value is not serializable and cannot be lazy") } return typeCheckSuccess() } reconcile(current: this["N"], value: T, parent: AnyObjectNode, subpath: string): this["N"] { if (this.loadedType) { current.die() return this.loadedType.instantiate( parent, subpath, parent.environment, value ) as this["N"] } return super.reconcile(current, value, parent, subpath) } } ================================================ FILE: src/types/utility-types/literal.ts ================================================ import { isPrimitive, createScalarNode, ISimpleType, TypeFlags, IValidationContext, IValidationResult, typeCheckSuccess, typeCheckFailure, isType, Primitives, AnyObjectNode, SimpleType, devMode } from "../../internal" import { assertArg } from "../../utils" /** * @internal * @hidden */ export class Literal extends SimpleType { readonly value: T readonly flags = TypeFlags.Literal constructor(value: T) { super(JSON.stringify(value)) this.value = value } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] ): this["N"] { return createScalarNode(this, parent, subpath, environment, initialValue) } describe() { return JSON.stringify(this.value) } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (isPrimitive(value) && value === this.value) { return typeCheckSuccess() } return typeCheckFailure( context, value, `Value is not a literal ${JSON.stringify(this.value)}` ) } } /** * `types.literal` - The literal type will return a type that will match only the exact given type. * The given value must be a primitive, in order to be serialized to a snapshot correctly. * You can use literal to match exact strings for example the exact male or female string. * * Example: * ```ts * const Person = types.model({ * name: types.string, * gender: types.union(types.literal('male'), types.literal('female')) * }) * ``` * * @param value The value to use in the strict equal check * @returns */ export function literal(value: S): ISimpleType { // check that the given value is a primitive assertArg(value, isPrimitive, "primitive", 1) return new Literal(value) } /** * Returns if a given value represents a literal type. * * @param type * @returns */ export function isLiteralType(type: unknown): type is ISimpleType { return isType(type) && (type.flags & TypeFlags.Literal) > 0 } ================================================ FILE: src/types/utility-types/maybe.ts ================================================ import { union, optional, IType, undefinedType, nullType, IAnyType, assertIsType } from "../../internal" const optionalUndefinedType = optional(undefinedType, undefined) const optionalNullType = optional(nullType, null) /** @hidden */ export interface IMaybeIType extends IType {} /** @hidden */ export interface IMaybe extends IMaybeIType {} /** @hidden */ export interface IMaybeNull extends IMaybeIType {} /** * `types.maybe` - Maybe will make a type nullable, and also optional. * The value `undefined` will be used to represent nullability. * * @param type * @returns */ export function maybe(type: IT): IMaybe { assertIsType(type, 1) return union(type, optionalUndefinedType) } /** * `types.maybeNull` - Maybe will make a type nullable, and also optional. * The value `null` will be used to represent no value. * * @param type * @returns */ export function maybeNull(type: IT): IMaybeNull { assertIsType(type, 1) return union(type, optionalNullType) } ================================================ FILE: src/types/utility-types/optional.ts ================================================ import { isStateTreeNode, IType, TypeFlags, isType, IValidationContext, IValidationResult, typecheckInternal, typeCheckSuccess, MstError, IAnyType, AnyObjectNode, BaseType, assertIsType, ExtractCSTWithSTN, devMode } from "../../internal" type IFunctionReturn = () => T type IOptionalValue = C | IFunctionReturn /** @hidden */ export type ValidOptionalValue = string | boolean | number | null | undefined /** @hidden */ export type ValidOptionalValues = [ValidOptionalValue, ...ValidOptionalValue[]] /** * @hidden * @internal */ export class OptionalValue< IT extends IAnyType, OptionalVals extends ValidOptionalValues > extends BaseType< IT["CreationType"] | OptionalVals[number], IT["SnapshotType"], IT["TypeWithoutSTN"] > { get flags() { return this._subtype.flags | TypeFlags.Optional } constructor( private readonly _subtype: IT, private readonly _defaultValue: IOptionalValue, readonly optionalValues: OptionalVals ) { super(_subtype.name) } describe() { return this._subtype.describe() + "?" } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { if (this.optionalValues.indexOf(initialValue) >= 0) { const defaultInstanceOrSnapshot = this.getDefaultInstanceOrSnapshot() return this._subtype.instantiate( parent, subpath, environment, defaultInstanceOrSnapshot ) } return this._subtype.instantiate(parent, subpath, environment, initialValue) } reconcile( current: this["N"], newValue: this["C"] | this["T"], parent: AnyObjectNode, subpath: string ): this["N"] { return this._subtype.reconcile( current, this.optionalValues.indexOf(newValue) < 0 && this._subtype.is(newValue) ? newValue : this.getDefaultInstanceOrSnapshot(), parent, subpath ) } getDefaultInstanceOrSnapshot(): this["C"] | this["T"] { const defaultInstanceOrSnapshot = typeof this._defaultValue === "function" ? (this._defaultValue as IFunctionReturn)() : this._defaultValue // while static values are already snapshots and checked on types.optional // generator functions must always be rechecked just in case if (typeof this._defaultValue === "function") { typecheckInternal(this, defaultInstanceOrSnapshot) } return defaultInstanceOrSnapshot } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { // defaulted values can be skipped if (this.optionalValues.indexOf(value) >= 0) { return typeCheckSuccess() } // bounce validation to the sub-type return this._subtype.validate(value, context) } isAssignableFrom(type: IAnyType) { return this._subtype.isAssignableFrom(type) } getSubTypes() { return this._subtype } } /** @hidden */ export type OptionalDefaultValueOrFunction = | IT["CreationType"] | IT["SnapshotType"] | (() => ExtractCSTWithSTN) /** @hidden */ export interface IOptionalIType extends IType< IT["CreationType"] | OptionalVals[number], IT["SnapshotType"], IT["TypeWithoutSTN"] > {} function checkOptionalPreconditions( type: IAnyType, defaultValueOrFunction: OptionalDefaultValueOrFunction ) { // make sure we never pass direct instances if (typeof defaultValueOrFunction !== "function" && isStateTreeNode(defaultValueOrFunction)) { throw new MstError( "default value cannot be an instance, pass a snapshot or a function that creates an instance/snapshot instead" ) } assertIsType(type, 1) if (devMode()) { // we only check default values if they are passed directly // if they are generator functions they will be checked once they are generated // we don't check generator function results here to avoid generating a node just for type-checking purposes // which might generate side-effects if (typeof defaultValueOrFunction !== "function") { typecheckInternal(type, defaultValueOrFunction) } } } export function optional( type: IT, defaultValueOrFunction: OptionalDefaultValueOrFunction ): IOptionalIType export function optional( type: IT, defaultValueOrFunction: OptionalDefaultValueOrFunction, optionalValues: OptionalVals ): IOptionalIType /** * `types.optional` - Can be used to create a property with a default value. * * Depending on the third argument (`optionalValues`) there are two ways of operation: * - If the argument is not provided, then if a value is not provided in the snapshot (`undefined` or missing), * it will default to the provided `defaultValue` * - If the argument is provided, then if the value in the snapshot matches one of the optional values inside the array then it will * default to the provided `defaultValue`. Additionally, if one of the optional values inside the array is `undefined` then a missing * property is also valid. * * Note that it is also possible to include values of the same type as the intended subtype as optional values, * in this case the optional value will be transformed into the `defaultValue` (e.g. `types.optional(types.string, "unnamed", [undefined, ""])` * will transform the snapshot values `undefined` (and therefore missing) and empty strings into the string `"unnamed"` when it gets * instantiated). * * If `defaultValue` is a function, the function will be invoked for every new instance. * Applying a snapshot in which the optional value is one of the optional values (or `undefined`/_not_ present if none are provided) causes the * value to be reset. * * Example: * ```ts * const Todo = types.model({ * title: types.string, * subtitle1: types.optional(types.string, "", [null]), * subtitle2: types.optional(types.string, "", [null, undefined]), * done: types.optional(types.boolean, false), * created: types.optional(types.Date, () => new Date()), * }) * * // if done is missing / undefined it will become false * // if created is missing / undefined it will get a freshly generated timestamp * // if subtitle1 is null it will default to "", but it cannot be missing or undefined * // if subtitle2 is null or undefined it will default to ""; since it can be undefined it can also be missing * const todo = Todo.create({ title: "Get coffee", subtitle1: null }) * ``` * * @param type * @param defaultValueOrFunction * @param optionalValues an optional array with zero or more primitive values (string, number, boolean, null or undefined) * that will be converted into the default. `[ undefined ]` is assumed when none is provided * @returns */ export function optional( type: IT, defaultValueOrFunction: OptionalDefaultValueOrFunction, optionalValues?: OptionalVals ): IOptionalIType { checkOptionalPreconditions(type, defaultValueOrFunction) return new OptionalValue( type, defaultValueOrFunction, optionalValues ? optionalValues : undefinedAsOptionalValues ) } const undefinedAsOptionalValues: [undefined] = [undefined] /** * Returns if a value represents an optional type. * * @template IT * @param type * @returns */ export function isOptionalType(type: unknown): type is IOptionalIType { return isType(type) && (type.flags & TypeFlags.Optional) > 0 } ================================================ FILE: src/types/utility-types/reference.ts ================================================ import { getStateTreeNode, isStateTreeNode, createScalarNode, IType, TypeFlags, IValidationContext, IValidationResult, typeCheckSuccess, typeCheckFailure, MstError, IAnyType, IAnyStateTreeNode, IAnyComplexType, Hook, IDisposer, maybe, isModelType, IMaybe, NodeLifeCycle, ReferenceIdentifier, normalizeIdentifier, getIdentifier, applyPatch, AnyNode, AnyObjectNode, SimpleType, assertIsType, isValidIdentifier, IStateTreeNode, devMode, isType, type IAnyModelType } from "../../internal" export type OnReferenceInvalidatedEvent = { parent: IAnyStateTreeNode invalidTarget: STN | undefined invalidId: ReferenceIdentifier replaceRef: (newRef: STN | null | undefined) => void removeRef: () => void cause: "detach" | "destroy" | "invalidSnapshotReference" } export type OnReferenceInvalidated = ( event: OnReferenceInvalidatedEvent ) => void function getInvalidationCause(hook: Hook): "detach" | "destroy" | undefined { switch (hook) { case Hook.beforeDestroy: return "destroy" case Hook.beforeDetach: return "detach" default: return undefined } } /** @hidden */ export type ReferenceT = IT["TypeWithoutSTN"] & IStateTreeNode> class StoredReference { readonly identifier!: ReferenceIdentifier node!: AnyNode private resolvedReference?: { node: AnyObjectNode lastCacheModification: string } constructor( value: ReferenceT | ReferenceIdentifier, private readonly targetType: IT ) { if (isValidIdentifier(value)) { this.identifier = value } else if (isStateTreeNode(value)) { const targetNode = getStateTreeNode(value) if (!targetNode.identifierAttribute) throw new MstError(`Can only store references with a defined identifier attribute.`) const id = targetNode.unnormalizedIdentifier if (id === null || id === undefined) { throw new MstError( `Can only store references to tree nodes with a defined identifier.` ) } this.identifier = id } else { throw new MstError( `Can only store references to tree nodes or identifiers, got: '${value}'` ) } } private updateResolvedReference(node: AnyNode) { const normalizedId = normalizeIdentifier(this.identifier) const root = node.root const lastCacheModification = root.identifierCache!.getLastCacheModificationPerId(normalizedId) if ( !this.resolvedReference || this.resolvedReference.lastCacheModification !== lastCacheModification ) { const { targetType } = this // reference was initialized with the identifier of the target const target = root.identifierCache!.resolve(targetType, normalizedId) if (!target) { throw new InvalidReferenceError( `[mobx-state-tree] Failed to resolve reference '${this.identifier}' to type '${this.targetType.name}' (from node: ${node.path})` ) } this.resolvedReference = { node: target!, lastCacheModification: lastCacheModification } } } get resolvedValue(): ReferenceT { this.updateResolvedReference(this.node) return this.resolvedReference!.node.value } } /** * @internal * @hidden */ export class InvalidReferenceError extends Error { constructor(m: string) { super(m) Object.setPrototypeOf(this, InvalidReferenceError.prototype) } } /** * @internal * @hidden */ export abstract class BaseReferenceType extends SimpleType< ReferenceIdentifier, ReferenceIdentifier, IT["TypeWithoutSTN"] > { readonly flags = TypeFlags.Reference constructor( protected readonly targetType: IT, private readonly onInvalidated?: OnReferenceInvalidated> ) { super(`reference(${targetType.name})`) } describe() { return this.name } isAssignableFrom(type: IAnyType): boolean { return this.targetType.isAssignableFrom(type) } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { return isValidIdentifier(value) ? typeCheckSuccess() : typeCheckFailure( context, value, "Value is not a valid identifier, which is a string or a number" ) } private fireInvalidated( cause: "detach" | "destroy" | "invalidSnapshotReference", storedRefNode: this["N"], referenceId: ReferenceIdentifier, refTargetNode: AnyObjectNode | null ) { // to actually invalidate a reference we need an alive parent, // since it is a scalar value (immutable-ish) and we need to change it // from the parent const storedRefParentNode = storedRefNode.parent if (!storedRefParentNode || !storedRefParentNode.isAlive) { return } const storedRefParentValue = storedRefParentNode.storedValue if (!storedRefParentValue) { return } this.onInvalidated!({ cause, parent: storedRefParentValue, invalidTarget: refTargetNode ? refTargetNode.storedValue : undefined, invalidId: referenceId, replaceRef(newRef) { applyPatch(storedRefNode.root.storedValue, { op: "replace", value: newRef, path: storedRefNode.path }) }, removeRef() { if (isModelType(storedRefParentNode.type)) { this.replaceRef(undefined as any) } else { applyPatch(storedRefNode.root.storedValue, { op: "remove", path: storedRefNode.path }) } } }) } private addTargetNodeWatcher( storedRefNode: this["N"], referenceId: ReferenceIdentifier ): IDisposer | undefined { // this will make sure the target node becomes created const refTargetValue = this.getValue(storedRefNode) if (!refTargetValue) { return undefined } const refTargetNode = getStateTreeNode(refTargetValue) const hookHandler = (_: AnyNode, refTargetNodeHook: Hook) => { const cause = getInvalidationCause(refTargetNodeHook) if (!cause) { return } this.fireInvalidated(cause, storedRefNode, referenceId, refTargetNode) } const refTargetDetachHookDisposer = refTargetNode.registerHook( Hook.beforeDetach, hookHandler ) const refTargetDestroyHookDisposer = refTargetNode.registerHook( Hook.beforeDestroy, hookHandler ) return () => { refTargetDetachHookDisposer() refTargetDestroyHookDisposer() } } protected watchTargetNodeForInvalidations( storedRefNode: this["N"], identifier: ReferenceIdentifier, customGetSet: ReferenceOptionsGetSet | undefined ) { if (!this.onInvalidated) { return } let onRefTargetDestroyedHookDisposer: IDisposer | undefined // get rid of the watcher hook when the stored ref node is destroyed // detached is ignored since scalar nodes (where the reference resides) cannot be detached storedRefNode.registerHook(Hook.beforeDestroy, () => { if (onRefTargetDestroyedHookDisposer) { onRefTargetDestroyedHookDisposer() } }) const startWatching = (sync: boolean) => { // re-create hook in case the stored ref gets reattached if (onRefTargetDestroyedHookDisposer) { onRefTargetDestroyedHookDisposer() } // make sure the target node is actually there and initialized const storedRefParentNode = storedRefNode.parent const storedRefParentValue = storedRefParentNode && storedRefParentNode.storedValue if (storedRefParentNode && storedRefParentNode.isAlive && storedRefParentValue) { let refTargetNodeExists: boolean if (customGetSet) { refTargetNodeExists = !!customGetSet.get(identifier, storedRefParentValue) } else { refTargetNodeExists = storedRefNode.root.identifierCache!.has( this.targetType, normalizeIdentifier(identifier) ) } if (!refTargetNodeExists) { // we cannot change the reference in sync mode // since we are in the middle of a reconciliation/instantiation and the change would be overwritten // for those cases just let the wrong reference be assigned and fail upon usage // (like current references do) // this means that effectively this code will only run when it is created from a snapshot if (!sync) { this.fireInvalidated( "invalidSnapshotReference", storedRefNode, identifier, null ) } } else { onRefTargetDestroyedHookDisposer = this.addTargetNodeWatcher( storedRefNode, identifier ) } } } if (storedRefNode.state === NodeLifeCycle.FINALIZED) { // already attached, so the whole tree is ready startWatching(true) } else { if (!storedRefNode.isRoot) { // start watching once the whole tree is ready storedRefNode.root.registerHook(Hook.afterCreationFinalization, () => { // make sure to attach it so it can start listening if (storedRefNode.parent) { storedRefNode.parent.createObservableInstanceIfNeeded() } }) } // start watching once the node is attached somewhere / parent changes storedRefNode.registerHook(Hook.afterAttach, () => { startWatching(false) }) } } } /** * @internal * @hidden */ export class IdentifierReferenceType extends BaseReferenceType { constructor(targetType: IT, onInvalidated?: OnReferenceInvalidated>) { super(targetType, onInvalidated) } getValue(storedRefNode: this["N"]) { if (!storedRefNode.isAlive) return undefined const storedRef: StoredReference = storedRefNode.storedValue return storedRef.resolvedValue as any } getSnapshot(storedRefNode: this["N"]) { const ref: StoredReference = storedRefNode.storedValue return ref.identifier } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { const identifier = isStateTreeNode(initialValue) ? getIdentifier(initialValue)! : initialValue const storedRef = new StoredReference(initialValue, this.targetType as any) const storedRefNode: this["N"] = createScalarNode( this, parent, subpath, environment, storedRef as any ) storedRef.node = storedRefNode this.watchTargetNodeForInvalidations(storedRefNode, identifier as string, undefined) return storedRefNode } reconcile( current: this["N"], newValue: this["C"] | this["T"], parent: AnyObjectNode, subpath: string ): this["N"] { if (!current.isDetaching && current.type === this) { const compareByValue = isStateTreeNode(newValue) const ref: StoredReference = current.storedValue if ( (!compareByValue && ref.identifier === newValue) || (compareByValue && ref.resolvedValue === newValue) ) { current.setParent(parent, subpath) return current } } const newNode = this.instantiate(parent, subpath, undefined, newValue) current.die() // noop if detaching return newNode } } /** * @internal * @hidden */ export class CustomReferenceType extends BaseReferenceType { constructor( targetType: IT, private readonly options: ReferenceOptionsGetSet, onInvalidated?: OnReferenceInvalidated> ) { super(targetType, onInvalidated) } getValue(storedRefNode: this["N"]) { if (!storedRefNode.isAlive) return undefined as any const referencedNode = this.options.get( storedRefNode.storedValue, storedRefNode.parent ? storedRefNode.parent.storedValue : null ) return referencedNode } getSnapshot(storedRefNode: this["N"]) { return storedRefNode.storedValue } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, newValue: this["C"] | this["T"] ): this["N"] { const identifier = isStateTreeNode(newValue) ? this.options.set(newValue as any, parent ? parent.storedValue : null) : newValue const storedRefNode: this["N"] = createScalarNode( this, parent, subpath, environment, identifier as any ) this.watchTargetNodeForInvalidations(storedRefNode, identifier as string, this.options) return storedRefNode } reconcile( current: this["N"], newValue: this["C"] | this["T"], parent: AnyObjectNode, subpath: string ): this["N"] { const newIdentifier = isStateTreeNode(newValue) ? this.options.set(newValue as any, current ? current.storedValue : null) : newValue if ( !current.isDetaching && current.type === this && current.storedValue === newIdentifier ) { current.setParent(parent, subpath) return current } const newNode = this.instantiate(parent, subpath, undefined, newIdentifier) current.die() // noop if detaching return newNode } } export interface ReferenceOptionsGetSet { get(identifier: ReferenceIdentifier, parent: IAnyStateTreeNode | null): ReferenceT set(value: ReferenceT, parent: IAnyStateTreeNode | null): ReferenceIdentifier } export interface ReferenceOptionsOnInvalidated { // called when the current reference is about to become invalid onInvalidated: OnReferenceInvalidated> } export type ReferenceOptions = | ReferenceOptionsGetSet | ReferenceOptionsOnInvalidated | (ReferenceOptionsGetSet & ReferenceOptionsOnInvalidated) /** @hidden */ export interface IReferenceType extends IType {} /** * `types.reference` - Creates a reference to another type, which should have defined an identifier. * See also the [reference and identifiers](https://github.com/mobxjs/mobx-state-tree#references-and-identifiers) section. */ export function reference( subType: IT, options?: ReferenceOptions ): IReferenceType { assertIsType(subType, 1) if (devMode()) { if (arguments.length === 2 && typeof arguments[1] === "string") { // istanbul ignore next throw new MstError( "References with base path are no longer supported. Please remove the base path." ) } } const getSetOptions = options ? (options as ReferenceOptionsGetSet) : undefined const onInvalidated = options ? (options as ReferenceOptionsOnInvalidated).onInvalidated : undefined if (getSetOptions && (getSetOptions.get || getSetOptions.set)) { if (devMode()) { if (!getSetOptions.get || !getSetOptions.set) { throw new MstError( "reference options must either contain both a 'get' and a 'set' method or none of them" ) } } return new CustomReferenceType( subType, { get: getSetOptions.get, set: getSetOptions.set }, onInvalidated ) } else { return new IdentifierReferenceType(subType, onInvalidated) } } /** * Returns if a given value represents a reference type. * * @param type * @returns */ export function isReferenceType(type: unknown): type is IReferenceType { return isType(type) && (type.flags & TypeFlags.Reference) > 0 } export function safeReference( subType: IT, options: (ReferenceOptionsGetSet | {}) & { acceptsUndefined: false onInvalidated?: OnReferenceInvalidated> } ): IReferenceType export function safeReference( subType: IT, options?: (ReferenceOptionsGetSet | {}) & { acceptsUndefined?: boolean onInvalidated?: OnReferenceInvalidated> } ): IMaybe> /** * `types.safeReference` - A safe reference is like a standard reference, except that it accepts the undefined value by default * and automatically sets itself to undefined (when the parent is a model) / removes itself from arrays and maps * when the reference it is pointing to gets detached/destroyed. * * The optional options parameter object accepts a parameter named `acceptsUndefined`, which is set to true by default, so it is suitable * for model properties. * When used inside collections (arrays/maps), it is recommended to set this option to false so it can't take undefined as value, * which is usually the desired in those cases. * Additionally, the optional options parameter object accepts a parameter named `onInvalidated`, which will be called when the reference target node that the reference is pointing to is about to be detached/destroyed * * Strictly speaking it is a `types.maybe(types.reference(X))` (when `acceptsUndefined` is set to true, the default) and * `types.reference(X)` (when `acceptsUndefined` is set to false), both of them with a customized `onInvalidated` option. * * @param subType * @param options * @returns */ export function safeReference( subType: IT, options?: (ReferenceOptionsGetSet | {}) & { acceptsUndefined?: boolean onInvalidated?: OnReferenceInvalidated> } ): IReferenceType | IMaybe> { const refType = reference(subType, { ...options, onInvalidated(ev) { if (options && options.onInvalidated) { options.onInvalidated(ev) } ev.removeRef() } }) if (options && options.acceptsUndefined === false) { return refType } else { return maybe(refType) } } ================================================ FILE: src/types/utility-types/refinement.ts ================================================ import { isStateTreeNode, getStateTreeNode, IValidationContext, IValidationResult, typeCheckSuccess, typeCheckFailure, isType, TypeFlags, IAnyType, AnyObjectNode, BaseType, ExtractNodeType, assertIsType, devMode } from "../../internal" import { assertIsString, assertIsFunction } from "../../utils" class Refinement extends BaseType< IT["CreationType"], IT["SnapshotType"], IT["TypeWithoutSTN"], ExtractNodeType > { get flags() { return this._subtype.flags | TypeFlags.Refinement } constructor( name: string, private readonly _subtype: IT, private readonly _predicate: (v: IT["CreationType"]) => boolean, private readonly _message: (v: IT["CreationType"]) => string ) { super(name) } describe() { return this.name } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { // create the child type return this._subtype.instantiate(parent, subpath, environment, initialValue) as any } isAssignableFrom(type: IAnyType) { return this._subtype.isAssignableFrom(type) } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { const subtypeErrors = this._subtype.validate(value, context) if (subtypeErrors.length > 0) return subtypeErrors const snapshot = isStateTreeNode(value) ? getStateTreeNode(value).snapshot : value if (!this._predicate(snapshot)) { return typeCheckFailure(context, value, this._message(value)) } return typeCheckSuccess() } reconcile( current: this["N"], newValue: this["C"] | this["T"], parent: AnyObjectNode, subpath: string ): this["N"] { return this._subtype.reconcile(current, newValue, parent, subpath) as any } getSubTypes() { return this._subtype } } export function refinement( name: string, type: IT, predicate: (snapshot: IT["CreationType"]) => boolean, message?: string | ((v: IT["CreationType"]) => string) ): IT export function refinement( type: IT, predicate: (snapshot: IT["CreationType"]) => boolean, message?: string | ((v: IT["CreationType"]) => string) ): IT /** * `types.refinement` - Creates a type that is more specific than the base type, e.g. `types.refinement(types.string, value => value.length > 5)` to create a type of strings that can only be longer then 5. * * @param name * @param type * @param predicate * @returns */ export function refinement(...args: any[]): IAnyType { const name = typeof args[0] === "string" ? args.shift() : isType(args[0]) ? args[0].name : null const type = args[0] const predicate = args[1] const message = args[2] ? args[2] : (v: any) => "Value does not respect the refinement predicate" // ensures all parameters are correct assertIsType(type, [1, 2]) assertIsString(name, 1) assertIsFunction(predicate, [2, 3]) assertIsFunction(message, [3, 4]) return new Refinement(name, type, predicate, message) } /** * Returns if a given value is a refinement type. * * @param type * @returns */ export function isRefinementType(type: unknown): type is IAnyType { return isType(type) && (type.flags & TypeFlags.Refinement) > 0 } ================================================ FILE: src/types/utility-types/snapshotProcessor.ts ================================================ import { IType, IAnyType, BaseType, isStateTreeNode, IValidationContext, IValidationResult, AnyObjectNode, TypeFlags, ExtractNodeType, assertIsType, isType, getSnapshot, devMode, ComplexType, typeCheckFailure, isUnionType, Instance, ObjectNode, MstError } from "../../internal" /** @hidden */ declare const $mstNotCustomized: unique symbol /** @hidden */ const $preProcessorFailed: unique symbol = Symbol("$preProcessorFailed") /** @hidden */ // tslint:disable-next-line:class-name export interface _NotCustomized { // only for typings readonly [$mstNotCustomized]: undefined } /** @hidden */ export type _CustomOrOther = Custom extends _NotCustomized ? Other : Custom class SnapshotProcessor extends BaseType< _CustomOrOther, _CustomOrOther, IT["TypeWithoutSTN"], ExtractNodeType > { get flags() { return this._subtype.flags | TypeFlags.SnapshotProcessor } constructor( private readonly _subtype: IT, private readonly _processors: ISnapshotProcessors, name?: string ) { super(name || _subtype.name) } describe() { return `snapshotProcessor(${this._subtype.describe()})` } private preProcessSnapshot(sn: this["C"]): IT["CreationType"] { if (this._processors.preProcessor) { return this._processors.preProcessor.call(null, sn) } return sn as any } private preProcessSnapshotSafe(sn: this["C"]): IT["CreationType"] | typeof $preProcessorFailed { try { return this.preProcessSnapshot(sn) } catch (e) { return $preProcessorFailed } } private postProcessSnapshot(sn: IT["SnapshotType"], node: this["N"]): this["S"] { if (this._processors.postProcessor) { return this._processors.postProcessor!.call(null, sn, node.storedValue) as any } return sn } private _fixNode(node: this["N"]): void { // the node has to use these methods rather than the original type ones proxyNodeTypeMethods(node.type, this, "create") if (node instanceof ObjectNode) { node.hasSnapshotPostProcessor = !!this._processors.postProcessor } const oldGetSnapshot = node.getSnapshot node.getSnapshot = () => this.postProcessSnapshot(oldGetSnapshot.call(node), node) as any if (!isUnionType(this._subtype)) { node.getReconciliationType = () => { return this } } } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { const processedInitialValue = isStateTreeNode(initialValue) ? initialValue : this.preProcessSnapshot(initialValue) const node = this._subtype.instantiate( parent, subpath, environment, processedInitialValue ) as any this._fixNode(node) return node } reconcile( current: this["N"], newValue: this["C"] | this["T"], parent: AnyObjectNode, subpath: string ): this["N"] { const node = this._subtype.reconcile( current, isStateTreeNode(newValue) ? newValue : this.preProcessSnapshot(newValue), parent, subpath ) as any if (node !== current) { this._fixNode(node) } return node } getSnapshot(node: this["N"], applyPostProcess: boolean = true): this["S"] { const sn = this._subtype.getSnapshot(node) return applyPostProcess ? this.postProcessSnapshot(sn, node) : sn } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { const processedSn = this.preProcessSnapshotSafe(value) if (processedSn === $preProcessorFailed) { return typeCheckFailure(context, value, "Failed to preprocess value") } return this._subtype.validate(processedSn, context) } getSubTypes() { return this._subtype } /** * MST considers a given value to "be" of a subtype is the value is either: * * 1. And instance of the subtype * 2. A valid snapshot *in* of the subtype * * Before v7, we used to also consider processed models (as in, SnapshotOut values of this). * This is no longer the case, and is more in line with our overall "is" philosophy, which you can * see in `src/core/type/type.ts:104` (assuming lines don't change too much). * * For additonal commentary, see discussion in https://github.com/mobxjs/mobx-state-tree/pull/2182 * * The `is` function specifically checks for `SnapshotIn` or `Instance` of a given type. * * @param thing * @returns */ is(thing: any): thing is any { const value = isType(thing) ? this._subtype : isStateTreeNode(thing) ? thing : this.preProcessSnapshotSafe(thing) if (value === $preProcessorFailed) { return false } return this._subtype.validate(value, [{ path: "", type: this._subtype }]).length === 0 } isAssignableFrom(type: IAnyType): boolean { return this._subtype.isAssignableFrom(type) } isMatchingSnapshotId(current: this["N"], snapshot: this["C"]): boolean { if (!(this._subtype instanceof ComplexType)) { return false } const processedSn = this.preProcessSnapshot(snapshot) return this._subtype.isMatchingSnapshotId(current as any, processedSn) } } function proxyNodeTypeMethods( nodeType: any, snapshotProcessorType: any, ...methods: (keyof SnapshotProcessor)[] ) { for (const method of methods) { nodeType[method] = snapshotProcessorType[method].bind(snapshotProcessorType) } } // public API /** * A type that has its snapshots processed. */ export interface ISnapshotProcessor extends IType< _CustomOrOther, _CustomOrOther, IT["TypeWithoutSTN"] > {} /** * Snapshot processors. */ export interface ISnapshotProcessors { /** * Function that transforms an input snapshot. */ preProcessor?(snapshot: _CustomOrOther): IT["CreationType"] /** * Function that transforms an output snapshot. * @param snapshot */ postProcessor?( snapshot: IT["SnapshotType"], node: Instance ): _CustomOrOther } /** * `types.snapshotProcessor` - Runs a pre/post snapshot processor before/after serializing a given type. * * [See known issue with `applySnapshot` and `preProcessSnapshot`](https://github.com/mobxjs/mobx-state-tree/issues/1317) * * Example: * ```ts * const Todo1 = types.model({ text: types.string }) * // in the backend the text type must be null when empty * interface BackendTodo { * text: string | null * } * * const Todo2 = types.snapshotProcessor(Todo1, { * // from snapshot to instance * preProcessor(snapshot: BackendTodo) { * return { * text: sn.text || ""; * } * }, * * // from instance to snapshot * postProcessor(snapshot, node): BackendTodo { * return { * text: !sn.text ? null : sn.text * } * } * }) * ``` * * @param type Type to run the processors over. * @param processors Processors to run. * @param name Type name, or undefined to inherit the inner type one. * @returns */ export function snapshotProcessor< IT extends IAnyType, CustomC = _NotCustomized, CustomS = _NotCustomized >( type: IT, processors: ISnapshotProcessors, name?: string ): ISnapshotProcessor { assertIsType(type, 1) if (devMode()) { if (processors.postProcessor && typeof processors.postProcessor !== "function") { // istanbul ignore next throw new MstError("postSnapshotProcessor must be a function") } if (processors.preProcessor && typeof processors.preProcessor !== "function") { // istanbul ignore next throw new MstError("preSnapshotProcessor must be a function") } } return new SnapshotProcessor(type, processors, name) } ================================================ FILE: src/types/utility-types/union.ts ================================================ import { IValidationContext, IValidationResult, typeCheckSuccess, typeCheckFailure, flattenTypeErrors, isType, TypeFlags, IType, MstError, isPlainObject, IAnyType, IValidationError, _NotCustomized, AnyObjectNode, BaseType, devMode, assertIsType, assertArg } from "../../internal" export type ITypeDispatcher = ( snapshot: Types[number]["SnapshotType"] ) => Types[number] export interface UnionOptions { /** * Whether or not to use eager validation. * * When `true`, the first matching type will be used. Otherwise, all types will be checked and the * validation will pass if and only if a single type matches. */ eager?: boolean /** * A function that returns the type to be used given an input snapshot. */ dispatcher?: ITypeDispatcher } /** * @internal * @hidden */ export class Union extends BaseType< _CustomCSProcessor, _CustomCSProcessor, Types[number]["TypeWithoutSTN"] > { private readonly _dispatcher?: ITypeDispatcher private readonly _eager: boolean = true get flags() { let result: TypeFlags = TypeFlags.Union this._types.forEach(type => { result |= type.flags }) return result } constructor( name: string, private readonly _types: Types, options?: UnionOptions ) { super(name) options = { eager: true, dispatcher: undefined, ...options } this._dispatcher = options.dispatcher if (!options.eager) this._eager = false } isAssignableFrom(type: IAnyType) { return this._types.some(subType => subType.isAssignableFrom(type)) } describe() { return "(" + this._types.map(factory => factory.describe()).join(" | ") + ")" } instantiate( parent: AnyObjectNode | null, subpath: string, environment: any, initialValue: this["C"] | this["T"] ): this["N"] { const type = this.determineType(initialValue, undefined) if (!type) throw new MstError("No matching type for union " + this.describe()) // can happen in prod builds return type.instantiate(parent, subpath, environment, initialValue) } reconcile( current: this["N"], newValue: this["C"] | this["T"], parent: AnyObjectNode, subpath: string ): this["N"] { const type = this.determineType(newValue, current.getReconciliationType()) if (!type) throw new MstError("No matching type for union " + this.describe()) // can happen in prod builds return type.reconcile(current, newValue, parent, subpath) } determineType( value: this["C"] | this["T"], reconcileCurrentType: IAnyType | undefined ): IAnyType | undefined { // try the dispatcher, if defined if (this._dispatcher) { return this._dispatcher(value) } // find the most accommodating type // if we are using reconciliation try the current node type first (fix for #1045) if (reconcileCurrentType) { if (reconcileCurrentType.is(value)) { return reconcileCurrentType } return this._types.filter(t => t !== reconcileCurrentType).find(type => type.is(value)) } else { return this._types.find(type => type.is(value)) } } isValidSnapshot(value: this["C"], context: IValidationContext): IValidationResult { if (this._dispatcher) { return this._dispatcher(value).validate(value, context) } const allErrors: IValidationError[][] = [] let applicableTypes = 0 for (let i = 0; i < this._types.length; i++) { const type = this._types[i] const errors = type.validate(value, context) if (errors.length === 0) { if (this._eager) return typeCheckSuccess() else applicableTypes++ } else { allErrors.push(errors) } } if (applicableTypes === 1) return typeCheckSuccess() return typeCheckFailure(context, value, "No type is applicable for the union").concat( flattenTypeErrors(allErrors) ) } getSubTypes() { return this._types } } /** * Transform _NotCustomized | _NotCustomized... to _NotCustomized, _NotCustomized | A | B to A | B * @hidden */ export type _CustomCSProcessor = Exclude extends never ? _NotCustomized : Exclude /** @hidden */ export interface ITypeUnion extends IType<_CustomCSProcessor, _CustomCSProcessor, T> {} export type IUnionType = ITypeUnion< Types[number]["CreationType"], Types[number]["SnapshotType"], Types[number]["TypeWithoutSTN"] > export function union(...types: Types): IUnionType export function union( options: UnionOptions, ...types: Types ): IUnionType /** * `types.union` - Create a union of multiple types. If the correct type cannot be inferred unambiguously from a snapshot, provide a dispatcher function of the form `(snapshot) => Type`. * * @param optionsOrType * @param otherTypes * @returns */ export function union( optionsOrType: UnionOptions | Types[number], ...otherTypes: Types ): IUnionType { const options = isType(optionsOrType) ? undefined : optionsOrType const types = (isType(optionsOrType) ? [optionsOrType, ...otherTypes] : otherTypes) as Types const name = "(" + types.map(type => type.name).join(" | ") + ")" // check all options if (devMode()) { if (options) { assertArg( options, o => isPlainObject(o), "object { eager?: boolean, dispatcher?: Function }", 1 ) } types.forEach((type, i) => { assertIsType(type, options ? i + 2 : i + 1) }) } return new Union(name, types, options) } /** * Returns if a given value represents a union type. * * @param type * @returns */ export function isUnionType(type: unknown): type is IUnionType { return isType(type) && (type.flags & TypeFlags.Union) > 0 } ================================================ FILE: src/utils.ts ================================================ import { isObservableArray, isObservableObject, _getGlobalState, defineProperty as mobxDefineProperty } from "mobx" import { Primitives } from "./core/type/type" const plainObjectString = Object.toString() /** * @internal * @hidden */ export const EMPTY_ARRAY: ReadonlyArray = Object.freeze([]) /** * @internal * @hidden */ export const EMPTY_OBJECT: {} = Object.freeze({}) /** * @internal * @hidden */ export const mobxShallow = _getGlobalState().useProxies ? { deep: false } : { deep: false, proxy: false } Object.freeze(mobxShallow) /** * A generic disposer. */ export type IDisposer = () => void /** * @internal * @hidden */ export class MstError extends Error { constructor(message = "Illegal state") { super(`[mobx-state-tree] ${message}`) } } /** * @internal * @hidden */ export function identity(_: any): any { return _ } /** * @internal * @hidden */ export function noop() {} /** * @internal * @hidden */ export const isInteger = Number.isInteger /** * @internal * @hidden */ export function isFloat(val: any) { return Number(val) === val && val % 1 !== 0 } /** * @internal * @hidden */ export function isFinite(val: any) { return Number.isFinite(val) } /** * @internal * @hidden */ export function isArray(val: any): val is any[] { return Array.isArray(val) || isObservableArray(val) } /** * @internal * @hidden */ export function asArray(val: undefined | null | T | T[] | ReadonlyArray): T[] { if (!val) return EMPTY_ARRAY as any as T[] if (isArray(val)) return val as T[] return [val] as T[] } /** * @internal * @hidden */ export function extend(a: A, b: B): A & B /** * @internal * @hidden */ export function extend(a: A, b: B, c: C): A & B & C /** * @internal * @hidden */ export function extend(a: A, b: B, c: C, d: D): A & B & C & D /** * @internal * @hidden */ export function extend(a: any, ...b: any[]): any /** * @internal * @hidden */ export function extend(a: any, ...b: any[]) { for (let i = 0; i < b.length; i++) { const current = b[i] for (let key in current) a[key] = current[key] } return a } /** * @internal * @hidden */ export function isPlainObject(value: any): value is { [k: string]: any } { if (value === null || typeof value !== "object") return false const proto = Object.getPrototypeOf(value) if (proto == null) return true return proto.constructor?.toString() === plainObjectString } /** * @internal * @hidden */ export function isMutable(value: any) { return ( value !== null && typeof value === "object" && !(value instanceof Date) && !(value instanceof RegExp) ) } /** * @internal * @hidden */ export function isPrimitive(value: any, includeDate = true): value is Primitives { return ( value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || (includeDate && value instanceof Date) ) } /** * @internal * @hidden * Freeze a value and return it (if not in production) */ export function freeze(value: T): T { if (!devMode()) return value return isPrimitive(value) || isObservableArray(value) ? value : Object.freeze(value) } /** * @internal * @hidden * Recursively freeze a value (if not in production) */ export function deepFreeze(value: T): T { if (!devMode()) return value freeze(value) if (isPlainObject(value)) { Object.keys(value).forEach(propKey => { if ( !isPrimitive((value as any)[propKey]) && !Object.isFrozen((value as any)[propKey]) ) { deepFreeze((value as any)[propKey]) } }) } return value } /** * @internal * @hidden */ export function isSerializable(value: any) { return typeof value !== "function" } /** * @internal * @hidden */ export function defineProperty(object: any, key: PropertyKey, descriptor: PropertyDescriptor) { isObservableObject(object) ? mobxDefineProperty(object, key, descriptor) : Object.defineProperty(object, key, descriptor) } /** * @internal * @hidden */ export function addHiddenFinalProp(object: any, propName: string, value: any) { defineProperty(object, propName, { enumerable: false, writable: false, configurable: true, value }) } /** * @internal * @hidden */ export function addHiddenWritableProp(object: any, propName: string, value: any) { defineProperty(object, propName, { enumerable: false, writable: true, configurable: true, value }) } /** * @internal * @hidden */ export type ArgumentTypes = F extends (...args: infer A) => any ? A : never /** * @internal * @hidden */ class EventHandler { private handlers: F[] = [] get hasSubscribers(): boolean { return this.handlers.length > 0 } register(fn: F, atTheBeginning = false): IDisposer { if (atTheBeginning) { this.handlers.unshift(fn) } else { this.handlers.push(fn) } return () => { this.unregister(fn) } } has(fn: F): boolean { return this.handlers.indexOf(fn) >= 0 } unregister(fn: F) { const index = this.handlers.indexOf(fn) if (index >= 0) { this.handlers.splice(index, 1) } } clear() { this.handlers.length = 0 } emit(...args: ArgumentTypes) { // make a copy just in case it changes const handlers = this.handlers.slice() handlers.forEach(f => f(...args)) } } /** * @internal * @hidden */ export class EventHandlers { private eventHandlers?: { [k in keyof E]?: EventHandler } hasSubscribers(event: keyof E): boolean { const handler = this.eventHandlers && this.eventHandlers[event] return !!handler && handler!.hasSubscribers } register(event: N, fn: E[N], atTheBeginning = false): IDisposer { if (!this.eventHandlers) { this.eventHandlers = {} } let handler = this.eventHandlers[event] if (!handler) { handler = this.eventHandlers[event] = new EventHandler() } return handler.register(fn, atTheBeginning) } has(event: N, fn: E[N]): boolean { const handler = this.eventHandlers && this.eventHandlers[event] return !!handler && handler!.has(fn) } unregister(event: N, fn: E[N]) { const handler = this.eventHandlers && this.eventHandlers[event] if (handler) { handler!.unregister(fn) } } clear(event: N) { if (this.eventHandlers) { delete this.eventHandlers[event] } } clearAll() { this.eventHandlers = undefined } emit(event: N, ...args: ArgumentTypes) { const handler = this.eventHandlers && this.eventHandlers[event] if (handler) { ;(handler!.emit as any)(...args) } } } const prototypeHasOwnProperty = Object.prototype.hasOwnProperty /** * @internal * @hidden */ export function hasOwnProperty(object: Object, propName: string) { return prototypeHasOwnProperty.call(object, propName) } /** * @internal * @hidden */ export function argsToArray(args: IArguments): any[] { const res = new Array(args.length) for (let i = 0; i < args.length; i++) res[i] = args[i] return res } /** * @internal * @hidden */ export function stringStartsWith(str: string, beginning: string) { return str.indexOf(beginning) === 0 } /** * @internal * @hidden */ export type DeprecatedFunction = Function & { ids?: { [id: string]: true } } /** * @internal * @hidden */ export const deprecated: DeprecatedFunction = function (id: string, message: string): void { // skip if running production if (!devMode()) return // warn if hasn't been warned before if (deprecated.ids && !deprecated.ids.hasOwnProperty(id)) { warnError("Deprecation warning: " + message) } // mark as warned to avoid duplicate warn message if (deprecated.ids) deprecated.ids[id] = true } deprecated.ids = {} /** * @internal * @hidden */ export function warnError(msg: string) { console.warn(new Error(`[mobx-state-tree] ${msg}`)) } /** * @internal * @hidden */ export function isTypeCheckingEnabled() { return ( devMode() || (typeof process !== "undefined" && process.env && process.env.ENABLE_TYPE_CHECK === "true") ) } /** * @internal * @hidden */ export function devMode() { return process.env.NODE_ENV !== "production" } /** * @internal * @hidden */ export function assertArg( value: T, fn: (value: T) => boolean, typeName: string, argNumber: number | number[] ) { if (devMode()) { if (!fn(value)) { // istanbul ignore next throw new MstError( `expected ${typeName} as argument ${asArray(argNumber).join(" or ")}, got ${value} instead` ) } } } /** * @internal * @hidden */ export function assertIsFunction(value: Function, argNumber: number | number[]) { assertArg(value, fn => typeof fn === "function", "function", argNumber) } /** * @internal * @hidden */ export function assertIsNumber( value: number, argNumber: number | number[], min?: number, max?: number ) { assertArg(value, n => typeof n === "number", "number", argNumber) if (min !== undefined) { assertArg(value, n => n >= min, `number greater than ${min}`, argNumber) } if (max !== undefined) { assertArg(value, n => n <= max, `number lesser than ${max}`, argNumber) } } /** * @internal * @hidden */ export function assertIsString(value: string, argNumber: number | number[], canBeEmpty = true) { assertArg(value, s => typeof s === "string", "string", argNumber) if (!canBeEmpty) { assertArg(value, s => s !== "", "not empty string", argNumber) } } /** * @internal * @hidden */ export function setImmediateWithFallback(fn: (...args: any[]) => void) { if (typeof queueMicrotask === "function") { queueMicrotask(fn) } else if (typeof setImmediate === "function") { setImmediate(fn) } else { setTimeout(fn, 1) } } ================================================ FILE: test-results/.gitkeep ================================================ ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "outDir": "lib/", "target": "es5", "sourceMap": false, "declaration": true, "module": "es2015", "removeComments": false, "moduleResolution": "node", "experimentalDecorators": true, "strict": true, "strictNullChecks": true, "strictFunctionTypes": true, "noImplicitAny": true, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noImplicitThis": true, "importHelpers": true, "stripInternal": true, "downlevelIteration": true, "lib": ["es6"], "useDefineForClassFields": true, "isolatedModules": true, "skipLibCheck": true }, "include": ["**/*.js", "**/*.ts"] } ================================================ FILE: tslint.json ================================================ { "extends": ["tslint-config-prettier"], "rules": { "class-name": true, "comment-format": [true, "check-space"], "curly": false, "indent": [true, "spaces"], "interface-name": false, "jsdoc-format": true, "no-consecutive-blank-lines": true, "no-debugger": true, "no-duplicate-variable": true, "no-eval": true, "no-internal-module": true, "no-shadowed-variable": true, "no-switch-case-fall-through": true, "no-unused-expression": true, "no-use-before-declare": false, "no-var-keyword": true, "one-line": [true, "check-open-brace", "check-whitespace", "check-catch"], "trailing-comma": false, "triple-equals": [true, "allow-null-check"], "variable-name": [true, "ban-keywords"] } } ================================================ FILE: typedocconfig.js ================================================ module.exports = { src: ["src/index.ts"], module: "commonjs", excludeNotExported: true, excludePrivate: true, excludeProtected: true, mode: "file", readme: "none", out: "./docs/API", theme: "docusaurus", tsconfig: "tsconfig.json", listInvalidSymbolLinks: true, mdHideSources: true // Note: TypeDoc uses the current git remote (e.g. your fork) for "Defined in" source links. // After generation, scripts/fix-docs-source-links.js rewrites them to https://github.com/mobxjs/mobx-state-tree/ } ================================================ FILE: website/core/Footer.js ================================================ /** * Copyright (c) 2017-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const React = require("react") class Footer extends React.Component { docUrl(doc, language) { const baseUrl = this.props.config.baseUrl const docsUrl = this.props.config.docsUrl const docsPart = `${docsUrl ? `${docsUrl}/` : ""}` const langPart = `${language ? `${language}/` : ""}` return `${baseUrl}${docsPart}${langPart}${doc}` } pageUrl(doc, language) { const baseUrl = this.props.config.baseUrl return baseUrl + (language ? `${language}/` : "") + doc } render() { return (

) } } module.exports = Footer ================================================ FILE: website/i18n/en.json ================================================ { "_comment": "This file is auto-generated by write-translations.js", "localized-strings": { "next": "Next", "previous": "Previous", "tagline": "Opinionated, transactional, MobX powered state container combining the best features of the immutable and mutable world for an optimal DX", "docs": { "API_header": { "title": "API_header" }, "API/index": { "title": "mobx-state-tree - v7.0.2", "sidebar_label": "Globals" }, "API/interfaces/customtypeoptions": { "title": "CustomTypeOptions", "sidebar_label": "CustomTypeOptions" }, "API/interfaces/functionwithflag": { "title": "FunctionWithFlag", "sidebar_label": "FunctionWithFlag" }, "API/interfaces/iactioncontext": { "title": "IActionContext", "sidebar_label": "IActionContext" }, "API/interfaces/iactionrecorder": { "title": "IActionRecorder", "sidebar_label": "IActionRecorder" }, "API/interfaces/iactiontrackingmiddleware2call": { "title": "IActionTrackingMiddleware2Call", "sidebar_label": "IActionTrackingMiddleware2Call" }, "API/interfaces/iactiontrackingmiddleware2hooks": { "title": "IActionTrackingMiddleware2Hooks", "sidebar_label": "IActionTrackingMiddleware2Hooks" }, "API/interfaces/iactiontrackingmiddlewarehooks": { "title": "IActionTrackingMiddlewareHooks", "sidebar_label": "IActionTrackingMiddlewareHooks" }, "API/interfaces/ianycomplextype": { "title": "IAnyComplexType", "sidebar_label": "IAnyComplexType" }, "API/interfaces/ianymodeltype": { "title": "IAnyModelType", "sidebar_label": "IAnyModelType" }, "API/interfaces/ianytype": { "title": "IAnyType", "sidebar_label": "IAnyType" }, "API/interfaces/ihooks": { "title": "IHooks", "sidebar_label": "IHooks" }, "API/interfaces/ijsonpatch": { "title": "IJsonPatch", "sidebar_label": "IJsonPatch" }, "API/interfaces/imiddlewareevent": { "title": "IMiddlewareEvent", "sidebar_label": "IMiddlewareEvent" }, "API/interfaces/imodelreflectiondata": { "title": "IModelReflectionData", "sidebar_label": "IModelReflectionData" }, "API/interfaces/imodelreflectionpropertiesdata": { "title": "IModelReflectionPropertiesData", "sidebar_label": "IModelReflectionPropertiesData" }, "API/interfaces/imodeltype": { "title": "IModelType", "sidebar_label": "IModelType" }, "API/interfaces/ipatchrecorder": { "title": "IPatchRecorder", "sidebar_label": "IPatchRecorder" }, "API/interfaces/ireversiblejsonpatch": { "title": "IReversibleJsonPatch", "sidebar_label": "IReversibleJsonPatch" }, "API/interfaces/iserializedactioncall": { "title": "ISerializedActionCall", "sidebar_label": "ISerializedActionCall" }, "API/interfaces/isimpletype": { "title": "ISimpleType", "sidebar_label": "ISimpleType" }, "API/interfaces/isnapshotprocessor": { "title": "ISnapshotProcessor", "sidebar_label": "ISnapshotProcessor" }, "API/interfaces/isnapshotprocessors": { "title": "ISnapshotProcessors", "sidebar_label": "ISnapshotProcessors" }, "API/interfaces/itype": { "title": "IType", "sidebar_label": "IType" }, "API/interfaces/ivalidationcontextentry": { "title": "IValidationContextEntry", "sidebar_label": "IValidationContextEntry" }, "API/interfaces/ivalidationerror": { "title": "IValidationError", "sidebar_label": "IValidationError" }, "API/interfaces/referenceoptionsgetset": { "title": "ReferenceOptionsGetSet", "sidebar_label": "ReferenceOptionsGetSet" }, "API/interfaces/referenceoptionsoninvalidated": { "title": "ReferenceOptionsOnInvalidated", "sidebar_label": "ReferenceOptionsOnInvalidated" }, "API/interfaces/unionoptions": { "title": "UnionOptions", "sidebar_label": "UnionOptions" }, "compare/context-reducer-vs-mobx-state-tree": { "title": "React Context vs. MobX-State-Tree" }, "concepts/actions": { "title": "Actions" }, "concepts/async-actions": { "title": "Asynchronous actions" }, "concepts/dependency-injection": { "title": "Dependency Injection" }, "concepts/listeners": { "title": "Listening to observables, snapshots, patches and actions", "sidebar_label": "Listening to changes" }, "concepts/middleware": { "title": "Middleware" }, "concepts/patches": { "title": "Patches" }, "concepts/using-react": { "title": "React and MST" }, "concepts/reconciliation": { "title": "Reconciliation" }, "concepts/references": { "title": "Identifiers and references" }, "concepts/snapshots": { "title": "Snapshots" }, "concepts/trees": { "title": "Types, models, trees & state" }, "concepts/views": { "title": "Derived values" }, "concepts/volatiles": { "title": "Volatile state" }, "intro/examples": { "title": "Examples" }, "intro/getting-started": { "title": "Getting Started Tutorial" }, "intro/installation": { "title": "Installation" }, "intro/philosophy": { "title": "Overview & Philosophy" }, "intro/welcome": { "title": "Welcome to MobX-State-Tree!" }, "overview/hooks": { "title": "Lifecycle hooks overview" }, "overview/types": { "title": "Types overview" }, "overview/api": { "title": "API overview" }, "recipes/auto-generated-property-setter-actions": { "title": "Auto-Generated Property Setter Actions" }, "recipes/mst-query": { "title": "Manage Asynchronous Data with mst-query" }, "recipes/pre-built-form-types-with-mst-form-type": { "title": "Pre-built Form Types with MST Form Type" }, "tips/circular-deps": { "title": "Handle circular dependencies between files and types using `late`", "sidebar_label": "Circular dependencies" }, "tips/faq": { "title": "Frequently Asked Questions" }, "tips/inheritance": { "title": "Simulate inheritance by using type composition", "sidebar_label": "Simulating inheritance" }, "tips/more-tips": { "title": "Miscellaneous Tips" }, "tips/resources": { "title": "Talks & Blogs" }, "tips/snapshots-as-values": { "title": "Using snapshots as values" }, "tips/typescript": { "title": "TypeScript and MST" } }, "links": { "Documentation": "Documentation", "TypeDocs": "TypeDocs", "Sponsor": "Sponsor", "GitHub": "GitHub" }, "categories": { "Introduction": "Introduction", "Basic Concepts": "Basic Concepts", "Advanced Concepts": "Advanced Concepts", "API Overview": "API Overview", "Tips": "Tips", "Compare": "Compare", "Recipes": "Recipes", "Interfaces": "Interfaces" } }, "pages-strings": { "Help Translate|recruit community translators for your project": "Help Translate", "Edit this Doc|recruitment message asking to edit the doc source": "Edit", "Translate this Doc|recruitment message asking to translate the docs": "Translate" } } ================================================ FILE: website/package.json ================================================ { "scripts": { "examples": "docusaurus-examples", "start": "docusaurus-start", "build": "docusaurus-build", "publish-gh-pages": "docusaurus-publish", "write-translations": "docusaurus-write-translations", "version": "docusaurus-version", "rename-version": "docusaurus-rename-version" }, "devDependencies": { "docusaurus": "^1.14.0" } } ================================================ FILE: website/sidebars.json ================================================ { "docs": { "Introduction": [ "intro/welcome", "intro/installation", "intro/getting-started", "intro/examples", "intro/philosophy" ], "Basic Concepts": [ "concepts/trees", "concepts/actions", "concepts/views", "concepts/using-react", "concepts/snapshots", "concepts/references", "concepts/async-actions" ], "Advanced Concepts": [ "concepts/patches", "concepts/listeners", "concepts/dependency-injection", "concepts/middleware", "concepts/reconciliation", "concepts/volatiles" ], "API Overview": [ "overview/types", "overview/api", "overview/hooks" ], "Tips": [ "tips/resources", "tips/contributing", "tips/faq", "tips/typescript", "tips/circular-deps", "tips/inheritance", "tips/snapshots-as-values", "tips/more-tips" ], "Compare": [ "compare/context-reducer-vs-mobx-state-tree" ], "Recipes": [ "recipes/auto-generated-property-setter-actions", "recipes/pre-built-form-types-with-mst-form-type", "recipes/mst-query" ] }, "mobx-state-tree": { "Introduction": [ "API/index" ], "Interfaces": [ "API/interfaces/customtypeoptions", "API/interfaces/functionwithflag", "API/interfaces/iactioncontext", "API/interfaces/iactionrecorder", "API/interfaces/iactiontrackingmiddleware2call", "API/interfaces/iactiontrackingmiddleware2hooks", "API/interfaces/iactiontrackingmiddlewarehooks", "API/interfaces/ianycomplextype", "API/interfaces/ianymodeltype", "API/interfaces/ianytype", "API/interfaces/ihooks", "API/interfaces/ijsonpatch", "API/interfaces/imiddlewareevent", "API/interfaces/imodelreflectiondata", "API/interfaces/imodelreflectionpropertiesdata", "API/interfaces/imodeltype", "API/interfaces/ipatchrecorder", "API/interfaces/ireversiblejsonpatch", "API/interfaces/iserializedactioncall", "API/interfaces/isimpletype", "API/interfaces/isnapshotprocessor", "API/interfaces/isnapshotprocessors", "API/interfaces/itype", "API/interfaces/ivalidationcontextentry", "API/interfaces/ivalidationerror", "API/interfaces/referenceoptionsgetset", "API/interfaces/referenceoptionsoninvalidated", "API/interfaces/unionoptions" ] } } ================================================ FILE: website/siteConfig.js ================================================ /** * Copyright (c) 2017-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ // See https://docusaurus.io/docs/site-config for all the possible // site configuration options. const siteConfig = { algolia: { apiKey: "b7b0cfe7d1c8fa6db6089df94a3128f1", indexName: "mobx-state-tree" }, title: "MobX-state-tree", // Title for your website. tagline: "Opinionated, transactional, MobX powered state container combining the best features of the immutable and mutable world for an optimal DX", url: "https://mobx-state-tree.js.org/", // Your website URL // baseUrl: "/mobx-state-tree/", baseUrl: "/", // Base URL for your project */ docsUrl: "", cname: "mobx-state-tree.js.org", editUrl: "https://github.com/mobxjs/mobx-state-tree/edit/master/docs/", // For github.io type URLs, you would set the url and baseUrl like: // url: 'https://facebook.github.io', // baseUrl: '/test-site/', // Used for publishing and more projectName: "mobx-state-tree", organizationName: "mobxjs", gaTrackingId: "UA-65632006-4", // For top-level user or org sites, the organization is still the same. // e.g., for the https://JoelMarcey.github.io site, it would be set like... // organizationName: 'JoelMarcey' // For no header links in the top nav bar -> headerLinks: [], headerLinks: [ { doc: "intro/welcome", label: "Documentation" }, { doc: "API/index", label: "TypeDocs" }, { href: "https://opencollective.com/mobx", label: "Sponsor" }, { href: "https://github.com/mobxjs/mobx-state-tree", label: "GitHub" } // {doc: "support", label: "Support mobx-state-tree"} ], /* path to images for header/footer */ headerIcon: "img/favicon.ico", footerIcon: "img/favicon.ico", favicon: "img/favicon.ico", /* Colors for website */ colors: { primaryColor: "#000", secondaryColor: "#ff7000" }, /* Custom fonts for website */ /* fonts: { myFont: [ "Times New Roman", "Serif" ], myOtherFont: [ "-apple-system", "system-ui" ] }, */ // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. copyright: `Copyright © ${new Date().getFullYear()} Michel Weststrate`, highlight: { // Highlight.js theme to use for syntax highlighting in code blocks. theme: "dracula" }, // Add custom scripts here that would be placed in MobX-state-tree If you are not redirected automatically, follow this link.