├── .circleci └── config.yml ├── .github └── workflows │ ├── publish-preview.yml │ ├── publish-release.yml │ └── synchronize-npm-tags.yml ├── .gitignore ├── README.md ├── package.json ├── packages └── microstates │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmignore │ ├── .projectile │ ├── CHANGELOG.md │ ├── CODE_OF_CONDUCT.md │ ├── LICENSE │ ├── README.md │ ├── README │ ├── TodoMVC In Redux and Microstates.png │ ├── TodoMVC In Redux and Microstates.pxd │ │ ├── QuickLook │ │ │ ├── Icon.tiff │ │ │ ├── Preview.tiff │ │ │ └── Thumbnail.tiff │ │ ├── data │ │ │ ├── 23199B64-0AE9-4232-8B6C-653E099E409C │ │ │ └── F6783923-71CB-4016-A223-63EDD1F25915 │ │ └── metadata.info │ ├── boolean-statechart.png │ ├── create-dom-element-fast.gif │ ├── microstates-logo.svg │ ├── microstates-todomvc.js │ ├── microstates-todomvc.png │ ├── redux-todomvc.js │ ├── redux-todomvc.png │ └── todomvc-redux-microstates.png │ ├── benchmarks │ ├── any.benchmark.js │ ├── array.benchmark.js │ ├── benchmark.js │ ├── index.js │ ├── number.benchmark.js │ ├── object.benchmark.js │ └── todomvc.benchmark.js │ ├── index.js │ ├── jsconfig.json │ ├── package.json │ ├── repl.js │ ├── rollup.config.js │ ├── src │ ├── cached-property.js │ ├── dsl.js │ ├── identity.js │ ├── lens.js │ ├── literal.js │ ├── meta.js │ ├── microstates.js │ ├── observable.js │ ├── parameterized.js │ ├── pathmap.js │ ├── query.js │ ├── reflection.js │ ├── relationship.js │ ├── storage.js │ ├── tree.js │ ├── types.js │ └── types │ │ ├── any.js │ │ ├── array.js │ │ ├── boolean.js │ │ ├── number.js │ │ ├── object.js │ │ ├── primitive.js │ │ └── string.js │ ├── tests │ ├── .eslintrc.json │ ├── constants.test.js │ ├── dsl.test.js │ ├── examples │ │ ├── authentication.test.js │ │ └── cart.test.js │ ├── identity.test.js │ ├── index.test.js │ ├── initialization.test.js │ ├── lens.test.js │ ├── literal.test.js │ ├── microstates.test.js │ ├── mount.test.js │ ├── observable.test.js │ ├── package.test.js │ ├── parameterized.test.js │ ├── pathmap.test.js │ ├── query.test.js │ ├── recursive.test.js │ ├── relationship.test.js │ ├── setup │ ├── todomvc.js │ ├── type-shifting.test.js │ └── types │ │ ├── array.test.js │ │ ├── boolean.test.js │ │ ├── number.test.js │ │ ├── object.test.js │ │ └── string.test.js │ └── yarn.lock └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | node: 5 | docker: 6 | - image: circleci/node:10 7 | 8 | commands: 9 | yarn: 10 | steps: 11 | - restore_cache: 12 | keys: 13 | - v1-dependencies-{{ checksum "yarn.lock" }} 14 | - v1-dependencies- 15 | 16 | - run: yarn install 17 | 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: v1-dependencies-{{ checksum "yarn.lock" }} 22 | 23 | jobs: 24 | test: 25 | executor: node 26 | steps: 27 | - checkout 28 | - yarn 29 | - run: yarn test 30 | 31 | lint: 32 | executor: node 33 | steps: 34 | - checkout 35 | - yarn 36 | - run: yarn lint 37 | 38 | coverage: 39 | executor: node 40 | steps: 41 | - checkout 42 | - yarn 43 | - run: yarn coverage 44 | - run: yarn coveralls 45 | 46 | workflows: 47 | version: 2.1 48 | build: 49 | jobs: 50 | - test 51 | - lint 52 | - coverage 53 | 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/publish-preview.yml: -------------------------------------------------------------------------------- 1 | name: Publish Preview 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - release-* 8 | 9 | jobs: 10 | preview: 11 | name: Publish Preview Package 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - uses: actions/setup-node@v2 18 | with: 19 | registry-url: https://registry.npmjs.org 20 | - name: NPM Publish Preview 21 | uses: thefrontside/actions/publish-pr-preview@v2 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.FRONTSIDEJACK_GITHUB_TOKEN }} 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release-* 8 | 9 | jobs: 10 | publish: 11 | name: Synchronize with NPM 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Tag and Publish 16 | uses: thefrontside/actions/synchronize-with-npm@v1.9 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.FRONTSIDEJACK_GITHUB_TOKEN }} 19 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/synchronize-npm-tags.yml: -------------------------------------------------------------------------------- 1 | name: Synchronize NPM Tags 2 | 3 | on: 4 | delete 5 | 6 | jobs: 7 | clean: 8 | name: Synchronize NPM Tags 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: thefrontside/actions/synchronize-npm-tags@v1.1 13 | env: 14 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | /packages/*/dist 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microstates-monorepo", 3 | "version": "0.0.0-monorepo", 4 | "description": "Composable State Primitives for JavaScript", 5 | "author": "Frontside Engineering ", 6 | "private": true, 7 | "workspaces": { 8 | "packages": [ 9 | "packages/*" 10 | ] 11 | }, 12 | "scripts": { 13 | "test": "yarn workspaces run test", 14 | "lint": "yarn workspaces run lint", 15 | "coverage": "yarn workspaces run coverage", 16 | "coveralls": "yarn workspaces run coveralls" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/microstates/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/microstates/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /benchmarks 4 | /README -------------------------------------------------------------------------------- /packages/microstates/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "parser": "babel-eslint", 10 | "parserOptions": { 11 | "experimentalDecorators": true, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "indent": ["error", 2, { 16 | "SwitchCase": 1 17 | }], 18 | "no-console": "error", 19 | "no-unused-vars": "error", 20 | "prefer-let/prefer-let": 2, 21 | "semi": ["error", "always"], 22 | "space-before-function-paren": ["error", { 23 | "anonymous": "never", 24 | "named": "never", 25 | "asyncArrow": "always" 26 | }] 27 | }, 28 | "plugins": ["prefer-let"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/microstates/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .esm-cache 3 | /dist 4 | .nyc_output/ 5 | coverage 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /packages/microstates/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/**/* 3 | !package.json 4 | !*.md 5 | -------------------------------------------------------------------------------- /packages/microstates/.projectile: -------------------------------------------------------------------------------- 1 | -/dist 2 | - /node_modules 3 | -------------------------------------------------------------------------------- /packages/microstates/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.15.1] - 2019-10-11 10 | 11 | ### Changed 12 | 13 | - This release is identical to 0.15.0 except that there was a 14 | publishing error and the resulting package released to NPM was 15 | malformed. This release is generated from the same source as v0.15.0 16 | 17 | ## [0.15.0] - 2019-10-08 18 | 19 | ### Added 20 | 21 | - Fine grained control of how related Microstates are instantiated 22 | via abstract relationships 23 | https://github.com/microstates/microstates.js/pull/358 24 | 25 | ## [0.14.0] - 2019-03-27 26 | 27 | ### Breaking 28 | 29 | - Don't store keys on the object type. Moved Object's properties to entries https://github.com/microstates/microstates.js/pull/338 30 | 31 | ### Changed 32 | 33 | - Iterator objects should not be wrapped in arrays https://github.com/microstates/microstates.js/pull/339 34 | - Don't try to reify a value if the iteration is finished. https://github.com/microstates/microstates.js/pull/323 35 | 36 | ### Fixed 37 | 38 | - Upgrading dependencies to upgrade version of [Funcadelic which fixed errors caused by Uglify removing names from typeclasses](https://github.com/cowboyd/funcadelic.js/pull/68) https://github.com/microstates/microstates.js/pull/343 39 | - Always derive store location value from its microstate value. https://github.com/microstates/microstates.js/pull/334 40 | 41 | ## [0.13.0] - 2019-02-14 42 | 43 | ### Changed 44 | 45 | - BREAKING: Transitions initiated from a microstate reference in the 46 | store, now return the new reference to the *same 47 | microstate*. Before, they always returned the root of the microstate 48 | tree, which made modelling side-effects difficult on deep microstate trees. 49 | - Upgraded Rollup and associated plugins to latest version (via 50 | greenkeeper) #306, #307, #309, #310 51 | - Fix up typos in README #308 (thanks @leonardodino) 52 | - Improved code coverage in unit tests#320 53 | - Remove several pieces of dead code that weren't serving any purpose 54 | #314, #318 55 | 56 | ### Fixed 57 | 58 | - Passing a microstate to `Array#push` and `Array#unshift` allowed 59 | that microstate to become part of `valueOf`. Now, array unwraps all 60 | arguments before performing any operations. 61 | 62 | ## [0.12.4] - 2018-12-12 63 | 64 | ### Fixed 65 | 66 | - Add explicit Profunctor class name to prevent the class name from being stripped by Uglifyjs https://github.com/microstates/microstates.js/pull/303 67 | 68 | ### Changed 69 | 70 | - Gather all transitions from the prototype chain https://github.com/microstates/microstates.js/pull/290 71 | 72 | ## [0.12.3] - 2018-12-12 73 | 74 | ### Changed 75 | 76 | - Gather all transitions from the prototype chain https://github.com/microstates/microstates.js/pull/290 77 | 78 | ## [0.12.2] - 2018-11-26 79 | 80 | ### Fixed 81 | 82 | - Ensure Identity maps non-enumerable properties https://github.com/microstates/microstates.js/pull/284 83 | 84 | ## [0.12.1] - 2018-11-18 85 | 86 | ### Added 87 | 88 | - ArrayType#remove transition https://github.com/microstates/microstates.js/pull/275 89 | - ObjectType#Symbol.iterator to allow iterating objects in queries https://github.com/microstates/microstates.js/pull/276 90 | 91 | ### Changed 92 | 93 | - No special case for the `set` transition https://github.com/microstates/microstates.js/pull/279 94 | 95 | ## [0.12.0] - 2018-10-31 96 | 97 | ### Fixed 98 | 99 | - Store should allow destructuring microstates https://github.com/microstates/microstates.js/pull/263 100 | 101 | ### Added 102 | 103 | - Added ObjectType#map and #ObjectType#filter transitions https://github.com/microstates/microstates.js/pull/225 104 | - Added ArrayType#sort transition https://github.com/microstates/microstates.js/pull/245 105 | - Added ArrayType#slice transition https://github.com/microstates/microstates.js/pull/249 106 | 107 | ### Changed 108 | 109 | - ArrayType#map is now stable. array.map(x => x) is no-op https://github.com/microstates/microstates.js/pull/239 110 | - ArrayType#filter is now stable. array.filter(x => true) is no-op https://github.com/microstates/microstates.js/pull/241 111 | - Identity/Store no longer calls observer on creation (behaviour of Observer.from remains unchanged) https://github.com/microstates/microstates.js/pull/255 112 | 113 | 114 | ## [0.11.0] - 2018-10-11 115 | 116 | ### Added 117 | - New benchmarks based on TodoMVC, runnable via `yarn bench` 118 | - Introduced the "femtostates" architecture. See below for breaking 119 | changes. See also 120 | https://github.com/microstates/microstates.js/pull/227 121 | - The opaque type `Any` is now exported from the main microstates 122 | module. This is the type of `create(null)` 123 | - The `Primitive` type which aliases a types `valueOf()` to the 124 | `.state` property is now exported frmo the main microstates module. 125 | - The builtin types that correspond to JavaScript builtins are now, `ArrayType`, 126 | `ObjectType`, `BooleanType`, `StringType`, and `NumberType`. 127 | 128 | ### Changed 129 | - `.state` property is no longer on every microstate, only on 130 | primitive types like `Number`, `String`, `Boolean`, and `Any`. In 131 | order to get at the JavaScript value enclosed by a microstate, use 132 | the `valueOf()` method exported by the main package. 133 | - All microstate properties are _completely_ lazily evaluated. 134 | - Array microstates cannot be accessed by index, but must use the 135 | iterable interface. 136 | - Type-shifting has been restricted to the `initialize` method. 137 | 138 | 139 | ## [0.10.1] - 2018-08-12 140 | 141 | ### Fixed 142 | - Make sure dist is included in the package. https://github.com/microstates/microstates.js/pull/206 143 | 144 | ## [0.10.0] - 2018-08-08 145 | 146 | ### Added 147 | - Implement Array.pop() transition https://github.com/microstates/microstates.js/pull/197 148 | 149 | ### Changed 150 | - Make transitions functions stable per location. https://github.com/microstates/microstates.js/pull/200 151 | 152 | ## [0.9.6] - 2018-08-04 153 | ### Added 154 | - Implicitly bind methods for microstates inside a store. 155 | https://github.com/microstates/microstates.js/pull/193 156 | - Allow consumers to use the Identity / Store API directly. 157 | https://github.com/microstates/microstates.js/pull/194 158 | 159 | ### Changed 160 | - Make redundant transitions fully idemptotent for Identities. 161 | https://github.com/microstates/microstates.js/pull/191 162 | 163 | ## [0.9.5] - 2018-08-03 164 | ### Changed 165 | - [BUGFIX] - this resolves completely the issues of syntatic sugar 166 | non-determinism without the need for any workarounds 167 | https://github.com/microstates/microstates.js/pull/189 168 | 169 | ## [0.9.4] - 2018-08-02 170 | ### Changed 171 | - [BUGFIX] - syntactic sugar has problems and our testcases were not 172 | catching them. Include a partial 173 | workaround. https://github.com/microstates/microstates.js/pull/187 174 | 175 | ## [0.9.3] - 2018-08-01 176 | ### Added 177 | - REPL for experimenting with, and demoing microstates. 178 | 179 | ### Changed 180 | - [BUGFIX] fix problem where nested microstates being set to the same 181 | value were breaking https://github.com/microstates/microstates.js/pull/183 182 | - Many, many fixes to the README. 183 | 184 | ## [0.9.2] - 2018-07-31 185 | ### Added 186 | - Export filter & reduce query functions and remove sum from Reducible https://github.com/microstates/microstates.js/pull/169 187 | - Added map query to Reducible and export it https://github.com/microstates/microstates.js/pull/172 188 | 189 | ## [0.9.1] - 2018-07-28 190 | ### Changed 191 | - Migrate to the picostates architecture. This introduces many, many 192 | breaking changes. See 193 | https://github.com/microstates/microstates.js/pull/158 for details. 194 | 195 | ## [0.8.1] - 2018-06-20 196 | ### Changed 197 | - Bump to funcadelic 0.5.1 #139 198 | 199 | ## [0.8.0] - 2018-06-29 200 | ### Added 201 | - [ENHANCEMENT] Allow putting and assigning microstates to objects #119 202 | - [ENHANCEMENT] Added formatting to microstate types #133 203 | - [ENHANCEMENT] Add ability to include microstates in from #115 204 | - [ENCHANCEMENT] Transitions are now auto bound #142 205 | 206 | ### Changed 207 | - [BREAKING] Removed automatic caching of getters and memoize getters dependency #139 208 | - [BREAKING] exposed use for middleware and map for array mapping #138 209 | - [BREAKING] Fix transitions of parameterized arrays #127 210 | - [BREAKING] Drop UMD build and fix cjs/es builds #141 211 | - [CHORE] Upgraded to funcadelic 0.5.0 #128 212 | - [CHORE] Added failing test for observable from arraylike #131 213 | - [CHORE] Removed momoize-getters dependency from package.json #140 214 | -------------------------------------------------------------------------------- /packages/microstates/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at oss@frontside.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /packages/microstates/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Charles Lowell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /packages/microstates/README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/microstates.svg)](https://www.npmjs.com/package/microstates) 2 | [![bundle size (minified + gzip)](https://badgen.net/bundlephobia/minzip/microstates)](https://bundlephobia.com/result?p=microstates) 3 | [![Build Status](https://circleci.com/gh/thefrontside/microstates/tree/master.svg?style=shield)](https://circleci.com/gh/thefrontside/microstates/tree/master) 4 | [![Coverage Status](https://coveralls.io/repos/github/thefrontside/microstates/badge.svg?branch=master)](https://coveralls.io/github/thefrontside/microstates?branch=master) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Chat on Discord](https://img.shields.io/discord/700803887132704931?Label=Discord)](https://discord.gg/GhREy5v) 7 | [![Created by The Frontside](https://img.shields.io/badge/created%20by-frontside-26abe8.svg)](https://frontside.com) 8 | 9 |

10 | Microstates Logo
Microstates 11 |

12 | 13 | Microstates makes working with pure functions over immutable data 14 | feel like working with the classic, mutable models we all know and love. 15 | 16 |
17 | Table of Contents 18 | 19 | 20 | - [Features](#features) 21 | - [Why Microstates?](#why-microstates) 22 | - [M in MVC](#m-in-mvc) 23 | - [Functional Models](#functional-models) 24 | - [What is a Microstate?](#what-is-a-microstate) 25 | - [Types and Type Composition](#types-and-type-composition) 26 | - [Creating your own microstates](#creating-your-own-microstates) 27 | - [Array Microstates](#array-microstates) 28 | - [Object Microstates](#object-microstates) 29 | - [Transitions](#transitions) 30 | - [Transitions for built-in types](#transitions-for-built-in-types) 31 | - [Type transitions](#type-transitions) 32 | - [Chaining transitions](#chaining-transitions) 33 | - [The `initialize` transition](#the-initialize-transition) 34 | - [The `set` transition](#the-set-transition) 35 | - [Transition scope](#transition-scope) 36 | - [State Machines](#state-machines) 37 | - [Explicit Transitions](#explicit-transitions) 38 | - [Transition methods](#transition-methods) 39 | - [Immutable Object vs Immutable Data Structure](#immutable-object-vs-immutable-data-structure) 40 | - [Framework Integrations](#framework-integrations) 41 | - [`microstates` npm package](#microstates-npm-package) 42 | - [create(Type, value): Microstate](#createtype-value-microstate) 43 | - [from(any): Microstate](#fromany-microstate) 44 | - [map(microstate, fn): Microstate](#mapmicrostate-fn-microstate) 45 | - [Streaming State](#streaming-state) 46 | - [Structural Sharing](#structural-sharing) 47 | - [Memoized Getters](#memoized-getters) 48 | - [Debounce no-op transitions](#debounce-no-op-transitions) 49 | - [Identity Constructors](#identity-constructors) 50 | - [Store(microstate, callback)](#storemicrostate-callback) 51 | - [Observable.from(microstate)](#observablefrommicrostate) 52 | - [The Vision of Microstates](#the-vision-of-microstates) 53 | - [Shared Solutions](#shared-solutions) 54 | - [Added Flexibility](#added-flexibility) 55 | - [Framework Agnostic Solutions](#framework-agnostic-solutions) 56 | - [FAQ](#faq) 57 | - [What if I can't use class syntax?](#what-if-i-cant-use-class-syntax) 58 | - [What if I can't use Class Properties?](#what-if-i-cant-use-class-properties) 59 | - [Run Tests](#run-tests) 60 | 61 | 62 |
63 | 64 |
65 | 🎬 Videos 66 | 67 | - [Building a shopping cart with Microstates.js](https://www.youtube.com/watch?v=N9iu8h0CzNY) - The Frontside YouTube - Taras Mankovski & Alex Regan 68 | - [State of Enjoyment](https://www.youtube.com/watch?v=zWEg4-bGot0) at Framework Summit 2018 - Charles Lowell 69 | - [Microstates: The Ember component of state management](https://www.youtube.com/watch?v=kt5aRmhaE2M&t=7s) at EmberATX 70 | - [Composable State Primitives for JavaScript with Charles Lowell & Taras Mankovski](https://www.youtube.com/watch?v=4dmcZrUdKx4) - Devchat.tv 71 | - [Composable State Management with Microstates.js](https://www.youtube.com/watch?v=g0-nf7m1Muo&t=1s) at Toronto.js - Taras Mankovski 72 |
73 | 74 |
75 | 💬 Chat 76 | 77 | [Join our community on Discord](https://discord.gg/GhREy5v). Everyone is welcome. If you're a new to programming join our #beginner channel where extra care is taken to support those who're just getting started. 78 | 79 |
80 | 81 | # Features 82 | 83 | With Microstates added to your project, you get: 84 | 85 | - 🍇 Composable type system 86 | - 🍱 Reusable state atoms 87 | - 💎 Pure immutable state transitions without writing reducers 88 | - ⚡️ Lazy and synchronous out of the box 89 | - 🦋 Most elegant way to express state machines 90 | - 🎯 Transpilation free type system 91 | - 🔭 Optional integration with Observables 92 | - ⚛ Use in Node.js, browser or React Native 93 | - 🔬 [It's tiny](https://bundlephobia.com/result?p=microstates) 94 | 95 | But, most importantly, Microstates makes working with state fun. 96 | 97 | **When was the last time you had fun working with state?** 98 | 99 | For many, the answer is probably never, because state management in JavaScript is an endless game of compromises. You can choose to go fully immutable and write endless reducers. You can go mutable and everything becomes an observable. Or you can `setState` and lose the benefits of serialization and time travel debugging. 100 | 101 | Unlike the view layer, where most frameworks agree on some variation of React's concept of components, none of the current crop of state management tools strike the same balance that React components introduced to the API. 102 | 103 | React components have a tiny API. They are functional, simple and extremely reusable. The tiny API gives you high productivity for little necessary knowledge. Functional components are predictable and easy to reason about. They are conceptually simple, but simplicity hides an underlying architecture geared for performance. Their simplicity, predictability and isolation makes them composable and reusable. 104 | 105 | These factors combined are what make React style components easy to work with and ultimately fun to write. A Tiny API abstracting a sophisticated architecture that delivers performance and is equally useful on small and big projects is the outcome that we set out to achieve for state management with Microstates. 106 | 107 | It's not easy to find the right balance between simplicity and power, but considering the importance of state management in web applications, we believe it's a worthy challenge. Checkout the [Vision of Microstates](#the-vision-of-microstates) section if you're interested in learning more about where we're going. 108 | 109 | 142 | 143 | # What is a Microstate? 144 | 145 | A Microstate is just an object that is created from a value and a type. The value is just data, and the type is what defines how you can transition that data from one form into the next. Unlike normal JavaScript objects, microstates are 100% immutable and cannot be changed. They can only derive new immutable microstates through one of their type's transitions. 146 | 147 | # Types and Type Composition 148 | 149 | Microstates comes out of the box with 5 primitive types: `Boolean`, `Number`, `String`, `Object` and `Array`. 150 | 151 | ```js 152 | import { create, valueOf } from 'microstates'; 153 | 154 | let meaningOfLifeAndEverything = create(Number, 42); 155 | console.log(meaningOfLifeAndEverything.state); 156 | //> 42 157 | 158 | let greeting = create(String, 'Hello World'); 159 | console.log(greeting.state); 160 | //> Hello World 161 | 162 | let isOpen = create(Boolean, true); 163 | console.log(isOpen.state); 164 | //> true 165 | 166 | // For Object and Array use microstates valueOf method 167 | let foo = create(Object, { foo: 'bar' }); 168 | console.log(valueOf(foo)); 169 | //> { foo: 'bar' } 170 | 171 | let numbers = create(Array, [1, 2, 3, 4]); 172 | console.log(valueOf(numbers)); 173 | //> [ 1, 2, 3, 4 ] 174 | ``` 175 | 176 | Apart from these basic types, every other type in Microstates is built by combining other types. So for example, to create a Person type you could define a JavaScript class with two properties: `name` which has type String and `age` which has type Number. 177 | 178 | ```js 179 | class Person { 180 | name = String; 181 | age = Number; 182 | } 183 | ``` 184 | 185 | Once you have a type, you can use that type to create as many people as your application requires: 186 | 187 | ```js 188 | import { create } from 'microstates'; 189 | 190 | let person = create(Person, { name: 'Homer', age: 39 }); 191 | ``` 192 | 193 | Every microstate created with a type of `Person` will be an object 194 | extending Person to have a `set()` method: 195 | 196 | ```txt 197 | +----------------------+ 198 | | | +--------------------+ 199 | | Microstate +-name--+ +-concat()-> 200 | | | | Microstate +-set()-> 201 | | | | +-state: 'Homer' 202 | | | +--------------------+ 203 | | | 204 | | | +--------------------+ 205 | | +-age---+ +-increment()-> 206 | | | | Microstate +-decrement()-> 207 | | | | +-set()-> 208 | | | | +-state: 39 209 | | | +--------------------+ 210 | | | 211 | | +-set()-> 212 | | | 213 | +----------------------+ 214 | ``` 215 | 216 | For the five built in types, Microstates automatically gives you transitions that you can use to change their value. You don't have to write any code to handle common operations. 217 | 218 | ## Creating your own microstates 219 | 220 | Types can be combined with other types freely and Microstates will take care of handling the transitions for you. This makes it possible to build complex data structures that accurately describe your domain. 221 | 222 | Let's define another type that uses the person type. 223 | 224 | ```js 225 | class Car { 226 | designer = Person; 227 | name = String; 228 | } 229 | 230 | let theHomerCar = create(Car, { 231 | designer: { name: 'Homer', age: 39 }, 232 | name: 'The Homer' 233 | }); 234 | ``` 235 | 236 | `theHomerCar` object will have the following shape, 237 | 238 | ```txt 239 | +-------------------+ +----------------------+ 240 | | | | | +--------------------+ 241 | | Microstate | | Microstate +-name--+ +-concat()-> 242 | | | | | | Microstate +-set()-> 243 | | +-designer--+ | | +-state: 'Homer' 244 | | | | | +--------------------+ 245 | | | | | 246 | | | | | +--------------------+ 247 | | | | +-age---+ +-increment()-> 248 | | | | | | Microstate +-decrement()-> 249 | | | | | | +-set()-> 250 | | | | | | +-state: 39 251 | | | | | +--------------------+ 252 | | | | | 253 | | | | +-set()-> 254 | | | | | 255 | | | +----------------------+ 256 | | | 257 | | | +--------------------+ 258 | | +-name------+ +-concat()-> 259 | | | | Microstate +-set()-> 260 | | | | +-state: 'The Homer' 261 | | | +--------------------+ 262 | | | 263 | | +-set()-> 264 | | | 265 | +-------------------+ 266 | ``` 267 | 268 | You can use the object dot notation to access sub microstates. Using the same example from above: 269 | 270 | ```js 271 | theHomerCar.designer; 272 | //> Microstate{ name: Microstate'Homer', age: Microstate39 } 273 | 274 | theHomerCar.designer.age.state; 275 | //> 39 276 | 277 | theHomerCar.name.state; 278 | //> The Homer 279 | ``` 280 | 281 | You can use the `valueOf()` function available from the microstates 282 | module to retrieve the underlying value represented by a microstate. 283 | 284 | ```js 285 | import { valueOf } from 'microstates'; 286 | 287 | valueOf(theHomerCar); 288 | //> { designer: { name: 'Homer', age: 39 }, name: 'The Homer' } 289 | ``` 290 | 291 | ## Array Microstates 292 | 293 | Quite often it is helpful to describe your data as a collection of types. For example, a blog might have an array of posts. To do this, you can use the array of type notation `[Post]`. This signals that Microstates of this type represent an array whose members are each of the `Post` type. 294 | 295 | ```js 296 | class Blog { 297 | posts = [Post]; 298 | } 299 | 300 | class Post { 301 | id = Number; 302 | title = String; 303 | } 304 | 305 | let blog = create(Blog, { 306 | posts: [ 307 | { id: 1, title: 'Hello World' }, 308 | { id: 2, title: 'Most fascinating blog in the world' } 309 | ] 310 | }); 311 | 312 | for (let post of blog.posts) { 313 | console.log(post); 314 | } 315 | //> Microstate{ id: 1, title: 'Hello World' } 316 | //> Microstate{ id: 2, title: 'Most fascinating blog in the world' } 317 | ``` 318 | 319 | When you're working with an array microstate, the shape of the Microstate is determined by the value. In this case, `posts` is created with two items which will, in turn, create a Microstate with two items. Each item will be a Microstate of type `Post`. If you push another item onto the `posts` Microstate, it'll be treated as a `Post`. 320 | 321 | ```js 322 | let blog2 = blog.posts.push({ id: 3, title: 'It is only getter better' }); 323 | 324 | for (let post of blog2.posts) { 325 | console.log(post); 326 | } 327 | 328 | //> Microstate{ id: 1, title: 'Hello World' } 329 | //> Microstate{ id: 2, title: 'Most fascinating blog in the world' } 330 | //> Microstate{ id: 3, title: 'It is only getter better' } 331 | ``` 332 | 333 | Notice how we didn't have to do any extra work to define the state 334 | transition of adding another post to the list? That's the power of 335 | composition! 336 | 337 | ## Object Microstates 338 | 339 | You can also create an object microstate with `{Post}`. The difference is that the collection is treated as an object. This can be helpful when creating normalized data stores. 340 | 341 | ```js 342 | class Blog { 343 | posts = { Post }; 344 | } 345 | 346 | class Post { 347 | id = Number; 348 | title = String; 349 | } 350 | 351 | let blog = create(Blog, { 352 | posts: { 353 | '1': { id: 1, title: 'Hello World' }, 354 | '2': { id: 2, title: 'Most fascinating blog in the world' } 355 | } 356 | }); 357 | 358 | blog.posts.entries['1']; 359 | //> Microstate{ id: 1, title: 'Hello World' } 360 | 361 | blog.posts.entries['2']; 362 | //> Microstate{ id: 2, title: 'Most fascinating blog in the world' } 363 | ``` 364 | 365 | Object type microstates have `Object` transitions, such as `assign`, `put` and `delete`. 366 | 367 | ```js 368 | let blog2 = blog.posts.put('3', { id: 3, title: 'It is only getter better' }); 369 | 370 | blog2.posts.entries['3']; 371 | //> Microstate{ id: 3, title: 'It is only getter better' } 372 | ``` 373 | 374 | # Transitions 375 | 376 | Transitions are the operations that let you derive a new state from an existing state. All transitions return another Microstate. You can use state charts to visualize microstates. For example, the `Boolean` type can be described with the following statechart. 377 | 378 | Boolean Statechart 379 | 380 | The `Boolean` type has a `toggle` transition which takes no arguments and creates a new microstate with the state that is opposite of the current state. 381 | 382 | Here is what this looks like with Microstates. 383 | 384 | ```js 385 | import { create } from 'microstates'; 386 | 387 | let bool = create(Boolean, false); 388 | 389 | bool.state; 390 | //> false 391 | 392 | let inverse = bool.toggle(); 393 | 394 | inverse.state; 395 | //> true 396 | ``` 397 | 398 | > _Pro tip_ Remember, Microstate transitions always return a Microstate. This is true both inside and outside the transition function. Using this convention can allow composition to reach crazy levels of complexity. 399 | 400 | Let's use a `Boolean` in another type and see what happens. 401 | 402 | ```js 403 | class App { 404 | name = String; 405 | notification = Modal; 406 | } 407 | 408 | class Modal { 409 | text = String; 410 | isOpen = Boolean; 411 | } 412 | 413 | let app = create(App, { 414 | name: 'Welcome to your app', 415 | notification: { 416 | text: 'Hello there', 417 | isOpen: false 418 | } 419 | }); 420 | 421 | let opened = app.notification.isOpen.toggle(); 422 | //> Microstate 423 | 424 | valueOf(opened); 425 | //> { 426 | // name: 'Welcome to your app', 427 | // notification: { 428 | // text: 'Hello there', 429 | // isOpen: true 430 | // }} 431 | ``` 432 | 433 | Microstate transitions always return the whole object. Notice how we invoked the boolean transition `app.notification.isOpen`, but we didn't get a new Boolean microstate? Instead, we got a completely new App where everything was the same except for that single toggled value. 434 | 435 | ## Transitions for built-in types 436 | 437 | The primitive types have predefined transitions: 438 | 439 | - `Boolean` 440 | - `toggle(): Microstate` - return a Microstate with opposite boolean value 441 | - `String` 442 | - `concat(str: String): Microstate` - return a Microstate with `str` added to the end of the current value 443 | - `Number` 444 | - `increment(step = 1: Number): Microstate` - return a Microstate with number increased by `step`, default is 1. 445 | - `decrement(step = 1: Number): Microstate` - return a Microstate with number decreased by `step`, default is 1. 446 | - `Object` 447 | - `assign(object): Microstate` - return a Microstate after merging object into current object. 448 | - `put(key: String, value: Any): Microstate` - return a Microstate after adding value at given key. 449 | - `delete(key: String): Microstate` - return a Microstate after removing property at given key. 450 | - `Array` 451 | - `map(fn: (Microstate) => Microstate): Microstate` - return a Microstate with mapping function applied to each element in the array. For each element, the mapping function will receive the microstate for that element. Any transitions performed in the mapping function will be included in the final result. 452 | - `push(value: any): Microstate` - return a Microstate with value added to the end of the array. 453 | - `pop(): Microstate` - return a Microstate with last element removed from the array. 454 | - `shift(): Microstate` - return a Microstate with element removed from the array. 455 | - `unshift(value: any): Microstate` - return a Microstate with value added to the beginning of the array. 456 | - `filter(fn: state => boolean): Microstate` - return a Microstate with filtered array. The predicate function will receive state of each element in the array. If you return a falsy value from the predicate, the item will be excluded from the returned microstate. 457 | - `clear(): Microstate` - return a microstate with an empty array. 458 | 459 | Many transitions on primitive types are similar to methods on original classes. The biggest difference is that transitions always return Microstates. 460 | 461 | ## Type transitions 462 | 463 | Define the transitions for your types using methods. Inside of a transition, you can invoke any transitions you like on sub microstates. 464 | 465 | ```js 466 | import { create } from 'microstates'; 467 | 468 | class Person { 469 | name = String; 470 | age = Number; 471 | 472 | changeName(name) { 473 | return this.name.set(name); 474 | } 475 | } 476 | 477 | let homer = create(Person, { name: 'Homer', age: 39 }); 478 | 479 | let lisa = homer.changeName('Lisa'); 480 | ``` 481 | 482 | ## Chaining transitions 483 | 484 | Transitions can be composed out of any number of subtransitions. This is often referred to as "batch transitions" or "transactions". Let's say that when we authenticate a session, we need to both store the token and indicate that the user is now authenticated. To do this, we can chain transitions. The result of the last operation will become a new microstate. 485 | 486 | ```js 487 | class Session { 488 | token = String; 489 | } 490 | 491 | class Authentication { 492 | session = Session; 493 | isAuthenticated = Boolean; 494 | 495 | authenticate(token) { 496 | return this.session.token.set(token).isAuthenticated.set(true); 497 | } 498 | } 499 | 500 | class App { 501 | authentication = Authentication; 502 | } 503 | 504 | let app = create(App, { authentication: {} }); 505 | 506 | let authenticated = app.authentication.authenticate('SECRET'); 507 | 508 | valueOf(authenticated); 509 | //> { authentication: { session: { token: 'SECRET' }, isAuthenticated: true } } 510 | ``` 511 | 512 | ## The `initialize` transition 513 | 514 | Just as every state machine must begin life in its "start state", so too must every microstate begin life in the right state. For those cases where the start state depends on some logic, there is the `initialize` transition. The `initialize` transition is just like any other transition, except that it will be automatically called within create on every Microstate that declares one. 515 | 516 | You can even use this mechanism to transition the microstate to one with a completely different type and value. 517 | 518 | For example: 519 | 520 | ```js 521 | class Person { 522 | firstName = String; 523 | lastName = String; 524 | 525 | initialize({ firstname, lastname } = {}) { 526 | let initialized = this; 527 | 528 | if (firstname) { 529 | initialized = initialized.firstName.set(firstname); 530 | } 531 | 532 | if (lastname) { 533 | initialized = initialized.lastName.set(lastname); 534 | } 535 | 536 | return initialized; 537 | } 538 | } 539 | ``` 540 | 541 | ## The `set` transition 542 | 543 | The `set` transition is the only transition that is available on all types. It can be used to replace the value of the current Microstate with another value. 544 | 545 | ```js 546 | import { create } from 'microstates'; 547 | 548 | let number = create(Number, 42).set(43); 549 | 550 | number.state; 551 | //> 43 552 | ``` 553 | 554 | 612 | 613 | > _Pro tip_: Microstates will never require you to understand Monads in order to use transitions, but if you're interested in learning about the primitives of functional programming that power Microstates, you may want to checkout [funcadelic.js](https://github.com/cowboyd/funcadelic.js). 614 | 615 | ## Transition scope 616 | 617 | Microstates are composable, and they work exactly the same no matter what other microstate they're a part of. For this reason, Microstate transitions only have access to their own transitions and the transitions of the microstates they contain. What they do _not_ have is access to their context. This is similar to how components work. The parent component can render children and pass data to them, but the child components do not have direct access to the parent component. The same principle applies in Microstates, so as a result, it benefits from the same advantages of isolation and composability that make components awesome. 618 | 619 | # State Machines 620 | 621 | A state machine is a system that has a predefined set of states. At any given point, the state machine can only be in one of these states. Each state has a predefined set of transitions that can be derived from that state. These constraints are beneficial to application architecture because they provide a way to identify application state and suggest how the application state can change. 622 | 623 | From its conception, Microstates was created to be the most convenient way to express state machines. The goal was to design an API that would eliminate the barrier of using state machines and allow for them to be composable. After almost two years of refinement, the result is an API that has evolved significantly beyond what we typically associate with code that expresses state machines. 624 | 625 | [xstate](https://github.com/davidkpiano/xstate) for example, is a great specimen of the classic state machine API. It's a fantastic library and it addresses the very real need for state machines and statecharts in modern applications. For purposes of contrast, we'll use it to illustrate the API choices that went into Microstates. 626 | 627 | ## Explicit Transitions 628 | 629 | Most state machine libraries focus on finding the next state given a configuration. For example, this [xstate](https://github.com/davidkpiano/xstate#finite-state-machines) declaration describes what state id to match when in a specific state. 630 | 631 | ```js 632 | import { Machine } from 'xstate'; 633 | 634 | const lightMachine = Machine({ 635 | key: 'light', 636 | initial: 'green', 637 | states: { 638 | green: { 639 | on: { 640 | TIMER: 'yellow' 641 | } 642 | }, 643 | yellow: { 644 | on: { 645 | TIMER: 'red' 646 | } 647 | }, 648 | red: { 649 | on: { 650 | TIMER: 'green' 651 | } 652 | } 653 | } 654 | }); 655 | ``` 656 | 657 | Microstates does not do any special state resolution. You explicitly declare what happens on a state transition. Here is what a similar state machine looks like in Microstates. 658 | 659 | ```js 660 | class LightMachine { 661 | color = String; 662 | 663 | initialize({ color = 'green' } = {}) { 664 | return this.color.set(color); 665 | } 666 | 667 | timer() { 668 | switch (this.color.state) { 669 | case 'green': return this.color.set('yellow'); 670 | case 'yellow': return this.color.set('red'); 671 | case 'red': 672 | default: 673 | return this.color.set('green'); 674 | } 675 | } 676 | } 677 | ``` 678 | 679 | With Microstates, you explicitly describe what happens on transition and define the matching mechanism. 680 | 681 | ## Transition methods 682 | 683 | `transitionTo` is often used by state machine libraries to trigger state transition. Here is an example with xstate library, 684 | 685 | ```js 686 | const nextState = lightMachine.transition('green', 'TIMER').value; 687 | 688 | //> 'yellow' 689 | ``` 690 | 691 | Microstates does not have such a method. Instead, it relies on vanilla JavaScript property lookup. The method invocation is equivalent to calling `transitionTo` with name of the transition. 692 | 693 | ```js 694 | import { create } from 'microstates'; 695 | 696 | let lightMachine = create(LightMachine); 697 | 698 | const nextState = lightMachine.timer(); 699 | 700 | nextState.color.state; 701 | //> 'yellow' 702 | ``` 703 | 704 | ## Immutable Object vs Immutable Data Structure 705 | 706 | When you create a state machine with xstate, you create an immutable object. When you invoke a transition on an `xstate` state machine, the value of the object is the ID of the next state. All of the concerns of immutable value change as a result of state change are left for you to handle manually. 707 | 708 | Microstates treats value as part of the state machine. It allows you to colocate your state transitions with reducers that change the value of the state. 709 | 710 | # Framework Integrations 711 | 712 | - [React.js](https://github.com/microstates/react) 713 | - [Ember.js](https://github.com/microstates/ember) 714 | - Create a PR if you created an integration that you'd like to add to this list. 715 | - Create an issue if you'd like help integrating Microstates with a framework 716 | 717 | # `microstates` npm package 718 | 719 | The `microstates` package provides the `Microstate` class and functions that operate on Microstate objects. 720 | 721 | You can import the `microstates` package using: 722 | 723 | ```bash 724 | npm install microstates 725 | 726 | # or 727 | 728 | yarn add microstates 729 | ``` 730 | 731 | Then import the libraries using: 732 | 733 | ```js 734 | import Microstate, { create, from, map } from 'microstates'; 735 | ``` 736 | 737 | ## create(Type, value): Microstate 738 | 739 | The `create` function is conceptually similar to `Object.create`. It creates a Microstate object from a type class and a value. This function is lazy, so it should be safe in most high performant operations even with complex and deeply nested data structures. 740 | 741 | ```js 742 | import { create } from 'microstates'; 743 | 744 | create(Number, 42); 745 | //> Microstate 746 | ``` 747 | 748 | ## from(any): Microstate 749 | 750 | `from` allows the conversion of any POJO (plain JavaScript object) into a Microstate. Once you've created a Microstate, you can perform operations on all properties of the value. 751 | 752 | ```js 753 | import { from } from 'microstates'; 754 | 755 | from('hello world'); 756 | //Microstate 757 | 758 | from(42).increment(); 759 | //> Microstate 760 | 761 | from(true).toggle(); 762 | //> Microstate 763 | 764 | from([1, 2, 3]); 765 | //> Microstate> 766 | 767 | from({ hello: 'world' }); 768 | //> Microstate 769 | ``` 770 | 771 | `from` is lazy, so you can consume any deeply nested POJO and 772 | Microstates will allow you to perform transitions with it. The cost of 773 | building the objects inside of Microstates is paid whenever you reach 774 | for a Microstate inside. For example, `let o = from({ a: { b: { c: 42 775 | }}})` doesn't do anything until you start to read the properties with 776 | dot notiation like `o.entries` or with iteration / destructuring. 777 | 778 | ```js 779 | let [[[[[[c]]]]]] = from({a: { b: {c: 42}}}); 780 | valueOf(c.increment()); 781 | 782 | // { a: { b: { c: 43 }}} 783 | 784 | let greeting = from({ hello: ['world']}); 785 | let [ world ] = greeting.entries.hello; 786 | valueOf(world.concat('!!!')); 787 | // { hello: [ 'world!!!' ]} 788 | ``` 789 | 790 | ## map(microstate, fn): Microstate 791 | 792 | The `map` function invokes the function for each microstate in an array microstate. It is usually used to map over an array of microstates and return an array of components. The mapping function will receive each microstate in the array. You can invoke transitions on each microstate as you would usually. 793 | 794 | ```js 795 | let numbers = create([Number], [1, 2, 3, 4]); 796 | 797 |
    798 | {map(numbers, number => ( 799 |
  • number.increment()}>{number.state}
  • 800 | ))} 801 |
; 802 | ``` 803 | 804 | # Streaming State 805 | 806 | A microstate represents a single immutable value with transitions to derive the next value. Microstates provides a mechanism called Identity that allows to emit stream of Microstates. When you create an Identity from a Microstate, you get an object that has the same shape the original microstate. Every composed microstate becomes an identity and every transition gets wrapped in side effect emitting behaviour specific to the identity's constructor. Identity wrapped Microstate offer the following benefits over raw Microstates. 807 | 808 | ## Structural Sharing 809 | 810 | A common performance optimization used by all reactive engines is to prevent re-renders for components who’s props have not changed. The most efficient way to determine if a value has not changed it to perform an exact equality check, for example: `prevValue === currentValue`. If the reference is the same, then consider the value unchanged. The Identity makes this possible with Microstates by internally managing how the Identity is constructed as a result of a transition. It will automatically determine which branches of microstates are unchanged and reuse previous identities for those branches. 811 | 812 | ## Memoized Getters 813 | 814 | Microstates are immutable which makes it safe for us to memoize computations that are derived off their state. Identies will automatically memoize getters and return previously computed value when the microstate backing the identity has not changed. When a transition is invoked on the identity, the part of the identity tree that are changed will be re-created effectively invalidating the cache for the changed parts of the identity. The getters will be recomputed for state that is changed. 815 | 816 | ## Debounce no-op transitions 817 | 818 | Identities automatically prevent unnecessary re-renders by debouncing transitions that do not change the value. This eliminates the need for `shouldComponentUpdate` hooks for pure components because it is safe to assume that a component that is re-rendering is re-rendering as a result of a transition that changed the value. In other words, if the current state and the state being transitioned to are the same, then a "new state" that would be the same is not emitted. 819 | 820 | ```js 821 | let id = Store(from({ name: 'Charles' }), next => { 822 | console.count('changed'); 823 | id = next; 824 | }); 825 | 826 | id.name.set('Charles'); 827 | id.name.set('Charles'); 828 | ``` 829 | 830 | The above transitions would be debounced because they do not change the value. The update callback would not be called, even though set operation is called twice. 831 | 832 | ## Identity Constructors 833 | 834 | Microstates comes with two Identity constructors: _Store_ and _Observable_. Store will send next identity to a callback. Observable will create a stream of identities and send next identity through the stream. 835 | 836 | ### Store(microstate, callback) 837 | 838 | Store identity constructor takes two arguments: microstate and a callback. It returns an identity. When a transition is invoked on the identity, the callback will receive the next identity. 839 | 840 | ```js 841 | import { Store, from, valueOf } from 'microstates'; 842 | 843 | let initial = create(Number, 42); 844 | 845 | let last; 846 | 847 | last = Store(initial, next => (last = next)); 848 | 849 | last.increment(); 850 | //> undefined 851 | // callback will be invoked syncronously on transition 852 | 853 | // last here will reference the last 854 | last.increment(); 855 | //> undefined 856 | 857 | valueOf(last); 858 | //> 44 859 | ``` 860 | 861 | The same mechanism can be used with React or any other reactive environment. 862 | 863 | ```js 864 | import React from 'react'; 865 | import { Store, create } from 'microstates'; 866 | 867 | class Counter extends React.Component { 868 | state = { 869 | last: Store(create(Number, 42), next => this.setState({ last: next })) 870 | }; 871 | render() { 872 | let { last } = this.state; 873 | return ( 874 | 875 | ); 876 | } 877 | } 878 | ``` 879 | 880 | ### Observable.from(microstate) 881 | 882 | Microstates provides an easy way to convert a Microstate which represents a single value into a Observable stream of values. This is done by passing a Microstate to Observable.from function. This function will return a Observable object with a subscribe method. You can subscribe to the stream by passing an observer to the subscribe function. Once you subscribe, you will synchronously receive a microstate with middleware installed that will cause the result of transitions to be pushed through the stream. 883 | 884 | You should be able to use to any implementation of Observables that supports Observer.from using symbol-observable. We'll use RxJS for our example. 885 | 886 | ```js 887 | import { from as observableFrom } from 'rxjs'; 888 | import { create } from 'microstates'; 889 | 890 | let homer = create(Person, { firstName: 'Homer', lastName: 'Simpson' }); 891 | 892 | let observable = observableFrom(homer); 893 | 894 | let last; 895 | let subscription = observable.subscribe(next => { 896 | // capture the next microstate coming through the stream 897 | last = next; 898 | }); 899 | 900 | last.firstName.set('Homer J'); 901 | 902 | valueOf(last); 903 | //> { firstName: 'Homer J', lastName: 'Simpson' } 904 | ``` 905 | 906 | # The Vision of Microstates 907 | 908 | What if switching frameworks were easy? What if a company could build domain specific code that worked across frameworks? Imagine what it would be like if your tools stayed with you as you progressed in your career as an engineer. This is the world that we hope to create with Microstates. 909 | 910 | ## Shared Solutions 911 | 912 | Imagine never having to write another normalized data store again because someone made a normalized data store Microstate that you can compose into your app's Microstate. 913 | 914 | In the future (not currently implemented), you will be able to write a normalized data store like this, 915 | 916 | ```js 917 | import Normalized from 'future-normalized-microstate'; 918 | 919 | class MyApp { 920 | store = Normalized.of(Product, User, Category); 921 | } 922 | ``` 923 | 924 | The knowledge about building normalized data stores is available in libraries like [Ember Data](https://github.com/emberjs/data), [Orbit.js](https://github.com/orbitjs/orbit), [Apollo](https://www.apollographql.com) and [urql](https://github.com/FormidableLabs/urql), yet many companies end up rolling their own because these tools are coupled to other stacks. 925 | 926 | As time and resources permit, we hope to create a solution that will be flexible enough for use in most applications. If you're interested in helping us with this, please reach out. 927 | 928 | ## Added Flexibility 929 | 930 | Imagine if your favourite Calendar component came with a Microstate that allowed you to customize the logic of the calendar without touching the rendered output. It might looks something like this, 931 | 932 | ```js 933 | import Calendar from 'awesome-calendar'; 934 | import { filter } from 'microstates'; 935 | 936 | class MyCalendar extends Calendar.Model { 937 | // make days as events 938 | days = Day.of([Event]); 939 | 940 | // component renders days from this property 941 | get visibleDays() { 942 | return filter(this.days, day => day.state.status !== 'finished'); 943 | } 944 | } 945 | 946 | ; 947 | ``` 948 | 949 | Currently, this is pseudocode, but Microstates was architected to allow for these kinds of solutions. 950 | 951 | ## Framework Agnostic Solutions 952 | 953 | Competition moves our industry forward but consensus builds ecosystems. 954 | 955 | Unfortunately, when it comes to the M(odel) of the MVC pattern, we are seeing neither competition nor consensus. Every framework has its own model layer that is not compatible with others. This makes it difficult to create truly portable solutions that can be used on all frameworks. 956 | 957 | It creates lock-in that is detrimental to the businesses that use these frameworks and to the developers who are forced to make career altering decisions before they fully understand their choices. 958 | 959 | We don't expect everyone to agree that Microstates is the right solution, but we would like to start the conversation about what a shared primitive for state management in JavaScript might look like. Microstates is our proposal. 960 | 961 | In many ways, Microstates is a beginning. We hope you'll join us for the ride and help us create a future where building stateful applications in JavaScript is much easier than it is today. 962 | 963 | # FAQ 964 | 965 | ## What if I can't use class syntax? 966 | 967 | Classes are functions in JavaScript, so you should be able to use a function to do most of the same things as you would with classes. 968 | 969 | ```js 970 | class Person { 971 | name = String; 972 | age = Number; 973 | } 974 | ``` 975 | 976 | ☝️ is equivalent to 👇 977 | 978 | ```js 979 | function Person() { 980 | this.name = String; 981 | this.age = Number; 982 | } 983 | ``` 984 | 985 | ## What if I can't use Class Properties? 986 | 987 | Babel compiles Class Properties into class constructors. If you can't use Class Properties, then you 988 | can try the following. 989 | 990 | ```js 991 | class Person { 992 | constructor() { 993 | this.name = String; 994 | this.age = Number; 995 | } 996 | } 997 | 998 | class Employee extends Person { 999 | constructor() { 1000 | super(); 1001 | this.boss = Person; 1002 | } 1003 | } 1004 | ``` 1005 | 1006 | # Run Tests 1007 | 1008 | ```shell 1009 | $ npm install 1010 | $ npm test 1011 | ``` 1012 | -------------------------------------------------------------------------------- /packages/microstates/README/TodoMVC In Redux and Microstates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/TodoMVC In Redux and Microstates.png -------------------------------------------------------------------------------- /packages/microstates/README/TodoMVC In Redux and Microstates.pxd/QuickLook/Icon.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/TodoMVC In Redux and Microstates.pxd/QuickLook/Icon.tiff -------------------------------------------------------------------------------- /packages/microstates/README/TodoMVC In Redux and Microstates.pxd/QuickLook/Preview.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/TodoMVC In Redux and Microstates.pxd/QuickLook/Preview.tiff -------------------------------------------------------------------------------- /packages/microstates/README/TodoMVC In Redux and Microstates.pxd/QuickLook/Thumbnail.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/TodoMVC In Redux and Microstates.pxd/QuickLook/Thumbnail.tiff -------------------------------------------------------------------------------- /packages/microstates/README/TodoMVC In Redux and Microstates.pxd/data/23199B64-0AE9-4232-8B6C-653E099E409C: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/TodoMVC In Redux and Microstates.pxd/data/23199B64-0AE9-4232-8B6C-653E099E409C -------------------------------------------------------------------------------- /packages/microstates/README/TodoMVC In Redux and Microstates.pxd/data/F6783923-71CB-4016-A223-63EDD1F25915: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/TodoMVC In Redux and Microstates.pxd/data/F6783923-71CB-4016-A223-63EDD1F25915 -------------------------------------------------------------------------------- /packages/microstates/README/TodoMVC In Redux and Microstates.pxd/metadata.info: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/TodoMVC In Redux and Microstates.pxd/metadata.info -------------------------------------------------------------------------------- /packages/microstates/README/boolean-statechart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/boolean-statechart.png -------------------------------------------------------------------------------- /packages/microstates/README/create-dom-element-fast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/create-dom-element-fast.gif -------------------------------------------------------------------------------- /packages/microstates/README/microstates-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/microstates/README/microstates-todomvc.js: -------------------------------------------------------------------------------- 1 | import { create } from "microstates"; 2 | 3 | const SHOW_ALL = ""; 4 | const SHOW_COMPLETED = "show_completed"; 5 | const SHOW_ACTIVE = "show_active"; 6 | 7 | const FILTER_OPTIONS = { 8 | [SHOW_ALL]: "All", 9 | [SHOW_ACTIVE]: "Active", 10 | [SHOW_COMPLETED]: "Completed" 11 | }; 12 | 13 | class TodoMVC { 14 | todos = [Todo]; 15 | newTodo = String; 16 | filter = String; 17 | 18 | get nextId() { 19 | return ( 20 | this.todos.reduce( 21 | (max, todo) => max.set(Math.max(todo.id.state, max.state)), 22 | 0 23 | ).todos.state + 1 24 | ); 25 | } 26 | 27 | get completed() { 28 | return this.todos.filter(({ completed }) => completed.state).todos; 29 | } 30 | 31 | get active() { 32 | return this.todos.filter(({ completed }) => !completed.state).todos; 33 | } 34 | 35 | get completedCount() { 36 | return this.completed.length; 37 | } 38 | 39 | get remainingCount() { 40 | return this.todos.length - this.completedCount.state; 41 | } 42 | 43 | get isAllComplete() { 44 | return this.todos.length > 0 && this.remainingCount.state === 0; 45 | } 46 | 47 | get hasTodos() { 48 | return this.todos.length > 0; 49 | } 50 | 51 | get filteredTodos() { 52 | switch (this.filter.state) { 53 | case SHOW_COMPLETED: 54 | return this.completed; 55 | case SHOW_ACTIVE: 56 | return this.active; 57 | case SHOW_ALL: 58 | default: 59 | return this.todos; 60 | } 61 | } 62 | 63 | insertNewTodo() { 64 | if (this.newTodo.state.length === 0) { 65 | return this; 66 | } else { 67 | return this.todos 68 | .push({ 69 | text: this.newTodo.state, 70 | id: this.nextId.state, 71 | completed: false 72 | }) 73 | .newTodo.set(""); 74 | } 75 | } 76 | 77 | clearCompleted() { 78 | return this.todos.filter(({ completed }) => !completed.state); 79 | } 80 | 81 | toggleAll() { 82 | return this.todos.map(todo => todo.completed.set(true)); 83 | } 84 | } 85 | 86 | class Todo { 87 | id = Number; 88 | text = String; 89 | completed = Boolean; 90 | 91 | edit() { 92 | return create(EditingTodo, this); 93 | } 94 | } 95 | 96 | class EditingTodo extends Todo { 97 | get editing() { 98 | return true; 99 | } 100 | 101 | save() { 102 | return create(Todo, this); 103 | } 104 | } 105 | 106 | let todomvc = create(TodoMVC, { 107 | todos: [{ id: 0, text: "Write Microstates Docs", completed: false }] 108 | }); -------------------------------------------------------------------------------- /packages/microstates/README/microstates-todomvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/microstates-todomvc.png -------------------------------------------------------------------------------- /packages/microstates/README/redux-todomvc.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux' 2 | import { createSelector } from 'reselect' 3 | 4 | const ADD_TODO = 'ADD_TODO' 5 | const DELETE_TODO = 'DELETE_TODO' 6 | const EDIT_TODO = 'EDIT_TODO' 7 | const COMPLETE_TODO = 'COMPLETE_TODO' 8 | const COMPLETE_ALL_TODOS = 'COMPLETE_ALL_TODOS' 9 | const CLEAR_COMPLETED = 'CLEAR_COMPLETED' 10 | const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' 11 | const SHOW_ALL = 'show_all' 12 | const SHOW_COMPLETED = 'show_completed' 13 | const SHOW_ACTIVE = 'show_active' 14 | 15 | const addTodo = text => ({ type: ADD_TODO, text }) 16 | const deleteTodo = id => ({ type: DELETE_TODO, id }) 17 | const editTodo = (id, text) => ({ type: EDIT_TODO, id, text }) 18 | const completeTodo = id => ({ type: COMPLETE_TODO, id }) 19 | const completeAllTodos = () => ({ type: COMPLETE_ALL_TODOS }) 20 | const clearCompleted = () => ({ type: CLEAR_COMPLETED }) 21 | const setVisibilityFilter = filter => ({ type: SET_VISIBILITY_FILTER, filter }) 22 | 23 | const getVisibilityFilter = state => state.visibilityFilter 24 | const getTodos = state => state.todos 25 | 26 | const visibilityFilter = (state = SHOW_ALL, action) => { 27 | switch (action.type) { 28 | case SET_VISIBILITY_FILTER: 29 | return action.filter 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | const getVisibleTodos = createSelector( 36 | [getVisibilityFilter, getTodos], 37 | (visibilityFilter, todos) => { 38 | switch (visibilityFilter) { 39 | case SHOW_ALL: 40 | return todos 41 | case SHOW_COMPLETED: 42 | return todos.filter(t => t.completed) 43 | case SHOW_ACTIVE: 44 | return todos.filter(t => !t.completed) 45 | default: 46 | throw new Error('Unknown filter: ' + visibilityFilter) 47 | } 48 | } 49 | ) 50 | 51 | const getCompletedTodoCount = createSelector( 52 | [getTodos], 53 | todos => ( 54 | todos.reduce((count, todo) => 55 | todo.completed ? count + 1 : count, 56 | 0 57 | ) 58 | ) 59 | ) 60 | 61 | const initialState = [ 62 | { 63 | text: 'Use Redux', 64 | completed: false, 65 | id: 0 66 | } 67 | ] 68 | 69 | function todos(state = initialState, action) { 70 | switch (action.type) { 71 | case ADD_TODO: 72 | return [ 73 | ...state, 74 | { 75 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 76 | completed: false, 77 | text: action.text 78 | } 79 | ] 80 | 81 | case DELETE_TODO: 82 | return state.filter(todo => 83 | todo.id !== action.id 84 | ) 85 | 86 | case EDIT_TODO: 87 | return state.map(todo => 88 | todo.id === action.id ? 89 | { ...todo, text: action.text } : 90 | todo 91 | ) 92 | 93 | case COMPLETE_TODO: 94 | return state.map(todo => 95 | todo.id === action.id ? 96 | { ...todo, completed: !todo.completed } : 97 | todo 98 | ) 99 | 100 | case COMPLETE_ALL_TODOS: 101 | const areAllMarked = state.every(todo => todo.completed) 102 | return state.map(todo => ({ 103 | ...todo, 104 | completed: !areAllMarked 105 | })) 106 | 107 | case CLEAR_COMPLETED: 108 | return state.filter(todo => todo.completed === false) 109 | 110 | default: 111 | return state 112 | } 113 | } 114 | 115 | const rootReducer = combineReducers({ 116 | todos, 117 | visibilityFilter 118 | }); 119 | 120 | const store = createStore(rootReducer) -------------------------------------------------------------------------------- /packages/microstates/README/redux-todomvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/redux-todomvc.png -------------------------------------------------------------------------------- /packages/microstates/README/todomvc-redux-microstates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thefrontside/microstates/0d97fe73ec9a43012a5c4ca096900dbbef98904c/packages/microstates/README/todomvc-redux-microstates.png -------------------------------------------------------------------------------- /packages/microstates/benchmarks/any.benchmark.js: -------------------------------------------------------------------------------- 1 | import { create, Store } from '../index'; 2 | import benchmark from './benchmark'; 3 | 4 | export default benchmark('Any', function AnyBenchmarks(suite) { 5 | suite.add('create(Any)', () => { 6 | create(); 7 | }); 8 | suite.add('create(Any, 5)', () => { 9 | create(undefined, 5); 10 | }); 11 | suite.add('create(Any).set(5)', () => { 12 | create().set(5); 13 | }); 14 | suite.add('Store(create(Any))', () => { 15 | Store(create()); 16 | }); 17 | suite.add('Store(create(Any, 5))', () => { 18 | Store(create(undefined, 5)); 19 | }); 20 | suite.add('Store(create().set(5))', () => { 21 | Store(create().set(5)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/microstates/benchmarks/array.benchmark.js: -------------------------------------------------------------------------------- 1 | import { Store, create } from '../index'; 2 | import benchmark from './benchmark'; 3 | 4 | const BIGARRAY = Array(100); 5 | BIGARRAY.fill(0); 6 | 7 | export default benchmark('Array', function(suite) { 8 | suite.add('create([])', () => { 9 | create([]); 10 | }); 11 | suite.add('create([], Array(100))', () => { 12 | create([], BIGARRAY); 13 | }); 14 | suite.add('create([]).set(Array(100))', () => { 15 | create([]).set(BIGARRAY); 16 | }); 17 | suite.add('Store(create([]))', () => { 18 | Store(create([])); 19 | }); 20 | suite.add('Store(create([], Array(100)))', () => { 21 | Store(create([], BIGARRAY)); 22 | }); 23 | suite.add('Store(create([]).set(Array(100)))', () => { 24 | Store(create([])).set(BIGARRAY); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/microstates/benchmarks/benchmark.js: -------------------------------------------------------------------------------- 1 | export default function(name, definition) { 2 | return function(suite) { 3 | return definition({ 4 | add(desc, operation) { 5 | return suite.add(desc, function() { 6 | this.suiteName = name; 7 | return operation(); 8 | }); 9 | } 10 | }); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /packages/microstates/benchmarks/index.js: -------------------------------------------------------------------------------- 1 | 2 | import any from './any.benchmark'; 3 | import number from './number.benchmark'; 4 | import array from './array.benchmark'; 5 | import object from './object.benchmark'; 6 | import todomvc from './todomvc.benchmark'; 7 | 8 | import Benchmark from 'benchmark'; 9 | import Table from 'cli-table'; 10 | 11 | const ENA = 'Err! N/A'; 12 | 13 | const suite = new Benchmark.Suite({ 14 | 15 | onComplete() { 16 | let table = new Table({ 17 | head: ['Suite', 'Benchmark', 'ops/sec', 'σ', 'sample size'] 18 | }); 19 | for (let i = 0; i < this.length; i++) { 20 | let benchmark = this[i]; 21 | let { stats } = benchmark; 22 | if (benchmark.error) { 23 | table.push([String(benchmark.suiteName), benchmark.name, ENA, ENA, stats.sample.length ]); 24 | } else { 25 | let { hz } = benchmark; 26 | let ops = Benchmark.formatNumber(hz.toFixed(hz < 100 ? 2 : 0)); 27 | let sigma = `± ${stats.rme.toFixed(2)} %`; 28 | table.push([String(benchmark.suiteName), benchmark.name, ops, sigma, stats.sample.length]); 29 | } 30 | } 31 | console.log(String(table)); 32 | }, 33 | onCycle(event) { 34 | if (!event.target.error) { 35 | console.log(String(event.target)); 36 | } 37 | }, 38 | onError(event) { 39 | let { error } = event.target; 40 | console.log(String(event.target), error); 41 | } 42 | }); 43 | 44 | any(suite); 45 | number(suite); 46 | array(suite); 47 | object(suite); 48 | todomvc(suite); 49 | 50 | suite.run({ async: true }); 51 | -------------------------------------------------------------------------------- /packages/microstates/benchmarks/number.benchmark.js: -------------------------------------------------------------------------------- 1 | import { Store, create } from '../index'; 2 | import benchmark from './benchmark'; 3 | 4 | export default benchmark('Number', function(suite) { 5 | suite.add('create(Number)', () => { 6 | create(Number); 7 | }); 8 | suite.add('create(Number, 42)', () => { 9 | create(Number, 42); 10 | }); 11 | suite.add('create(Number).set(42)', () => { 12 | create(Number).set(42); 13 | }); 14 | suite.add('Store(create(Number))', () => { 15 | Store(create(Number)); 16 | }); 17 | suite.add('Store(create(Number, 42))', () => { 18 | Store(create(Number, 42)); 19 | }); 20 | 21 | suite.add('Store(create(Number)).set(42)', () => { 22 | Store(create(Number)).set(42); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/microstates/benchmarks/object.benchmark.js: -------------------------------------------------------------------------------- 1 | import { Store, create } from '../index'; 2 | import benchmark from './benchmark'; 3 | 4 | const BIGOBJECT = {}; 5 | for (let i = 0; i < 100; i++) { 6 | BIGOBJECT[`key-${i}`] = `value-${i}`; 7 | } 8 | 9 | 10 | export default benchmark('Object', function(suite) { 11 | suite.add('create({})', () => { 12 | create({}); 13 | }); 14 | suite.add('create({}).set({})', () => { 15 | create({}).set({}); 16 | }); 17 | suite.add('create({}, BIGOBJECT)', () => { 18 | create({}, BIGOBJECT); 19 | }); 20 | suite.add('create({}).set(BIGOBJECT)', () => { 21 | create({}).set(BIGOBJECT); 22 | }); 23 | suite.add('Store(create({}))', () => { 24 | Store(create({})); 25 | }); 26 | suite.add('Store(create({}, BIGOBJECT))', () => { 27 | Store(create({}, BIGOBJECT)); 28 | }); 29 | suite.add('Store(create({}).set(BIGOBJECT))', () => { 30 | Store(create({})).set(BIGOBJECT); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/microstates/benchmarks/todomvc.benchmark.js: -------------------------------------------------------------------------------- 1 | import { Store, create } from '../index'; 2 | import benchmark from './benchmark'; 3 | import { TodoMVC } from '../tests/todomvc'; 4 | 5 | 6 | const MUCH_TODO = { todos: [] }; 7 | for (let i = 0; i < 100; i++) { 8 | MUCH_TODO.todos.push({ 9 | title: 'Todo #{i}', 10 | completed: Math.random() > 0.5 ? true : false 11 | }); 12 | } 13 | const ONE_MORE_TODO = { title: 'One more thing', completed: false }; 14 | 15 | export default benchmark('TodoMVC', function(suite) { 16 | suite.add('Store(create(TodoMVC, MUCH_TODO)); //100 todos', () => { 17 | Store(create(TodoMVC, MUCH_TODO)); 18 | }); 19 | let store = create(TodoMVC, MUCH_TODO); 20 | suite.add('store.todos.push(ONE_MORE_TODO); //add to a list of 100', () => { 21 | store.todos.push(ONE_MORE_TODO); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/microstates/index.js: -------------------------------------------------------------------------------- 1 | export { create } from './src/microstates'; 2 | export { default as from } from './src/literal'; 3 | export { map, filter, reduce, find } from './src/query'; 4 | export { default as Store } from './src/identity'; 5 | export { metaOf, valueOf } from './src/meta'; 6 | export { relationship } from './src/relationship.js'; 7 | export * from './src/types'; 8 | -------------------------------------------------------------------------------- /packages/microstates/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } -------------------------------------------------------------------------------- /packages/microstates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microstates", 3 | "version": "0.15.1", 4 | "description": "Composable State Primitives for JavaScript", 5 | "keywords": [ 6 | "lens", 7 | "state-machine", 8 | "immutable", 9 | "composable", 10 | "models", 11 | "state" 12 | ], 13 | "homepage": "https://github.com/thefrontside/microstates#readme", 14 | "bugs": { 15 | "url": "https://github.com/thefrontside/microstates/issues" 16 | }, 17 | "license": "MIT", 18 | "author": "Charles Lowell , Taras Mankovski ", 19 | "files": [ 20 | "src", 21 | "README.md", 22 | "dist" 23 | ], 24 | "main": "dist/microstates.cjs.js", 25 | "module": "dist/microstates.es.js", 26 | "unpkg": "dist/microstates.umd.js", 27 | "browser": "dist/microstates.umd.js", 28 | "repository": "git+ssh://git@github.com/thefrontside/microstates.git", 29 | "scripts": { 30 | "start": "node repl.js", 31 | "lint": "eslint ./", 32 | "test": "mocha --recursive -r tests/setup tests", 33 | "coverage": "nyc --reporter=html --reporter=text npm run test", 34 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 35 | "build": "rollup -c", 36 | "bench": "node -r ./tests/setup benchmarks/index.js", 37 | "prepack": "npm run build" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.1.6", 41 | "@babel/plugin-proposal-class-properties": "^7.1.0", 42 | "@babel/preset-env": "^7.0.0", 43 | "@babel/register": "^7.0.0", 44 | "babel-eslint": "^10.0.1", 45 | "benchmark": "^2.1.4", 46 | "cli-table": "^0.3.1", 47 | "coveralls": "^3.0.2", 48 | "eslint": "^6.0.0", 49 | "eslint-plugin-prefer-let": "^1.0.1", 50 | "expect": "^25.1.0", 51 | "mocha": "^6.0.0", 52 | "nyc": "^14.0.0", 53 | "object.getownpropertydescriptors": "^2.0.3", 54 | "rollup": "^1.3.2", 55 | "rollup-plugin-babel": "^4.3.1", 56 | "rollup-plugin-commonjs": "^10.0.0", 57 | "rollup-plugin-filesize": "^7.0.0", 58 | "rollup-plugin-node-resolve": "^5.0.0", 59 | "rollup-plugin-replace": "^2.1.0", 60 | "rxjs": "^6.2.1" 61 | }, 62 | "dependencies": { 63 | "funcadelic": "^0.5.0", 64 | "symbol-observable": "^1.2.0" 65 | }, 66 | "nyc": { 67 | "exclude": [ 68 | "**/tests" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/microstates/repl.js: -------------------------------------------------------------------------------- 1 | /* global vm */ 2 | let babel = require('@babel/core'); 3 | let repl = require('repl'); 4 | 5 | require('./tests/setup'); 6 | 7 | // this code lifted from the @babel/node package 8 | // https://github.com/babel/babel/blob/master/packages/babel-node/src/_babel-node.js#L104-L117 9 | const _eval = function(code, filename) { 10 | code = code.trim(); 11 | if (!code) return undefined; 12 | 13 | code = babel.transform(code, { 14 | filename: filename, 15 | }).code; 16 | 17 | return vm.runInThisContext(code, { 18 | filename: filename, 19 | }); 20 | }; 21 | 22 | 23 | // This code lifted from the @babel/node package 24 | // https://github.com/babel/babel/blob/master/packages/babel-node/src/_babel-node.js#L206-L221 25 | function replEval(code, context, filename, callback) { 26 | let err; 27 | let result; 28 | 29 | try { 30 | if (code[0] === "(" && code[code.length - 1] === ")") { 31 | code = code.slice(1, -1); // remove "(" and ")" 32 | } 33 | 34 | result = _eval(code, filename); 35 | } catch (e) { 36 | err = e; 37 | } 38 | 39 | callback(err, result); 40 | } 41 | 42 | // put all of the microstate functions into the global scope 43 | Object.assign(global, require('./index')); 44 | 45 | //start the repl using the @babel/node 46 | repl.start({ 47 | eval: replEval, 48 | useGlobal: true, 49 | }); 50 | -------------------------------------------------------------------------------- /packages/microstates/rollup.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require("./package.json"); 2 | const babel = require("rollup-plugin-babel"); 3 | const filesize = require("rollup-plugin-filesize"); 4 | const resolve = require("rollup-plugin-node-resolve"); 5 | const commonjs = require('rollup-plugin-commonjs'); 6 | const replace = require('rollup-plugin-replace'); 7 | 8 | const external = [ 9 | "funcadelic", 10 | "symbol-observable" 11 | ]; 12 | 13 | const babelPlugin = babel({ 14 | babelrc: false, 15 | comments: false, 16 | plugins: ["@babel/plugin-proposal-class-properties"], 17 | presets: [ 18 | [ 19 | "@babel/preset-env", 20 | { 21 | modules: false 22 | } 23 | ] 24 | ] 25 | }); 26 | 27 | const fileSize = filesize(); 28 | 29 | module.exports = [ 30 | { 31 | input: "index.js", 32 | output: { 33 | name: 'Microstates', 34 | file: pkg.unpkg, 35 | format: "umd" 36 | }, 37 | plugins: [ 38 | replace({ 39 | "process.env.NODE_ENV": JSON.stringify('production') 40 | }), 41 | resolve({ 42 | module: false, 43 | main: true 44 | }), 45 | babel({ 46 | babelrc: false, 47 | comments: false, 48 | plugins: ["@babel/plugin-proposal-class-properties"], 49 | presets: [ 50 | [ 51 | "@babel/preset-env", 52 | { 53 | targets: { 54 | chrome: "58", 55 | }, 56 | modules: false 57 | } 58 | ] 59 | ] 60 | }), 61 | commonjs(), 62 | fileSize 63 | ] 64 | }, 65 | { 66 | input: "index.js", 67 | external, 68 | output: { 69 | file: pkg.main, 70 | format: "cjs", 71 | sourcemap: true 72 | }, 73 | plugins: [ 74 | resolve(), 75 | babel({ 76 | babelrc: false, 77 | comments: false, 78 | plugins: ["@babel/plugin-proposal-class-properties"], 79 | presets: [ 80 | [ 81 | "@babel/preset-env", 82 | { 83 | targets: { 84 | node: "6" 85 | }, 86 | modules: false 87 | } 88 | ] 89 | ] 90 | }), 91 | fileSize 92 | ] 93 | }, 94 | { 95 | input: "index.js", 96 | external, 97 | output: { file: pkg.module, format: "es", sourcemap: true }, 98 | plugins: [ 99 | resolve(), 100 | babelPlugin, 101 | fileSize 102 | ] 103 | } 104 | ]; 105 | -------------------------------------------------------------------------------- /packages/microstates/src/cached-property.js: -------------------------------------------------------------------------------- 1 | export default function CachedProperty(key, reify) { 2 | let enumerable = true; 3 | let configurable = true; 4 | return { 5 | enumerable, 6 | configurable, 7 | get() { 8 | let value = reify(this); 9 | Object.defineProperty(this, key, { enumerable, value }); 10 | return value; 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/microstates/src/dsl.js: -------------------------------------------------------------------------------- 1 | import { Any, ObjectType, ArrayType, BooleanType, NumberType, StringType } from './types'; 2 | import Primitive from './types/primitive'; 3 | 4 | class DSL { 5 | constructor(rules = []) { 6 | this.rules = rules; 7 | } 8 | 9 | expand(value) { 10 | for (let rule of this.rules) { 11 | let result = rule.call(this, value); 12 | if (result && typeof result.Type === 'function') { 13 | return result; 14 | } else { 15 | continue; 16 | } 17 | } 18 | return { Type: Any, value }; 19 | } 20 | 21 | use(rule) { 22 | return new DSL(this.rules.concat(rule)); 23 | } 24 | } 25 | 26 | export default new DSL() 27 | .use(function matchBuiltins(value) { 28 | switch(value) { 29 | case Object: 30 | return { Type: ObjectType, value: {} }; 31 | case Array: 32 | return { Type: ArrayType, value: [] }; 33 | case Boolean: 34 | return { Type: BooleanType, value: false }; 35 | case Number: 36 | return { Type: NumberType, value: 0 }; 37 | } 38 | // TIL switch doesn't work on String ¯\_(ツ)_/¯ 39 | if (value === String) { 40 | return { Type: StringType, value: '' }; 41 | } 42 | 43 | }) 44 | .use(function matchArrays(value) { 45 | if (Array.isArray(value) && value.length < 2) { 46 | let [ T ] = value; 47 | let Type = T != null ? ArrayType.of(this.expand(T).Type) : ArrayType; 48 | return { Type, value: [] }; 49 | } 50 | }) 51 | .use(function matchObjects(value) { 52 | if (value != null && typeof value === 'object' && Object.keys(value).length < 2) { 53 | let [ key ] = Object.keys(value); 54 | if (key == null) { 55 | return { Type: ObjectType, value: {} }; 56 | } else { 57 | let { Type: childType } = this.expand(value[key]); 58 | if (childType.isConstant) { 59 | return undefined; 60 | } else { 61 | return {Type: ObjectType.of(childType), value: {} }; 62 | } 63 | } 64 | } 65 | }) 66 | .use(function matchCustomTypes(value) { 67 | if (typeof value === 'function') { 68 | return { Type: value, value: undefined }; 69 | } 70 | }) 71 | .use(function matchConstants(value) { 72 | return { Type: Constant(value), value }; 73 | }); 74 | 75 | const Constant = () => class Constant extends Primitive { 76 | static isConstant = true; 77 | 78 | }; 79 | -------------------------------------------------------------------------------- /packages/microstates/src/identity.js: -------------------------------------------------------------------------------- 1 | import { valueOf } from './meta'; 2 | 3 | import Storage from './storage'; 4 | import Pathmap from './pathmap'; 5 | 6 | export default function Identity(microstate, observe = x => x) { 7 | let { Type } = microstate.constructor; 8 | let pathmap = Pathmap(Type, new Storage(valueOf(microstate), () => observe(pathmap.get()))); 9 | return pathmap.get(); 10 | } 11 | -------------------------------------------------------------------------------- /packages/microstates/src/lens.js: -------------------------------------------------------------------------------- 1 | import { Functor, map, Semigroup } from 'funcadelic'; 2 | 3 | import { childAt } from './tree'; 4 | 5 | class Box { 6 | static get of() { 7 | return (...args) => new this(...args); 8 | } 9 | 10 | static unwrap(box) { 11 | return box.value; 12 | } 13 | 14 | constructor(value) { 15 | this.value = value; 16 | } 17 | } 18 | 19 | const { unwrap } = Box; 20 | 21 | class Id extends Box {} 22 | 23 | class Const extends Box {} 24 | 25 | Functor.instance(Id, { 26 | map(fn, id) { 27 | return Id.of(fn(id.value)); 28 | } 29 | }); 30 | 31 | Functor.instance(Const, { 32 | map(fn, constant) { 33 | return constant; 34 | } 35 | }); 36 | 37 | export function compose(f, g) { 38 | return (...x) => f(g(...x)); 39 | } 40 | 41 | export function view(lens, context) { 42 | let get = compose(unwrap, lens(Const.of)); 43 | return get(context); 44 | } 45 | 46 | export function over(lens, fn, context) { 47 | let update = compose(unwrap, lens(compose(Id.of, fn))); 48 | return update(context); 49 | } 50 | 51 | export function set(lens, value, context) { 52 | return over(lens, () => value, context); 53 | } 54 | 55 | export function Lens(get, set) { 56 | return f => context => { 57 | return map(value => set(value, context), f(get(context))); 58 | }; 59 | } 60 | 61 | export const transparent = Lens(x => x, y => y); 62 | 63 | export function At(property, container) { 64 | let get = context => context != null ? childAt(property, context) : undefined; 65 | let set = (part, whole) => { 66 | let context = whole == null ? (Array.isArray(container) ? [] : {}) : whole; 67 | if (part === context[property]) { 68 | return context; 69 | } else if (Array.isArray(context)) { 70 | let clone = context.slice(); 71 | clone[Number(property)] = part; 72 | return clone; 73 | } else { 74 | return Semigroup.for(Object).append(context, {[property]: part}); 75 | } 76 | }; 77 | 78 | return Lens(get, set); 79 | } 80 | 81 | export function Path(path) { 82 | return path.reduce((lens, key) => compose(lens, At(key)), transparent); 83 | } 84 | -------------------------------------------------------------------------------- /packages/microstates/src/literal.js: -------------------------------------------------------------------------------- 1 | import { create } from './microstates'; 2 | 3 | class Literal { 4 | initialize(value) { 5 | if (value == null) { 6 | return this; 7 | } 8 | value = value.valueOf(); 9 | switch (typeof value) { 10 | case "number": 11 | return create(Number, value); 12 | case "string": 13 | return create(String, value); 14 | case "boolean": 15 | return create(Boolean, value); 16 | default: 17 | if (Array.isArray(value)) { 18 | return create([Literal], value); 19 | } else { 20 | return create({Literal}, value); 21 | } 22 | } 23 | } 24 | } 25 | 26 | export default (value) => create(Literal, value); 27 | -------------------------------------------------------------------------------- /packages/microstates/src/meta.js: -------------------------------------------------------------------------------- 1 | import { At, compose, transparent, over, view } from './lens'; 2 | 3 | export class Meta { 4 | static symbol = Symbol('Meta'); 5 | static data = At(Meta.symbol); 6 | static lens = compose(Meta.data, At('lens')); 7 | static path = compose(Meta.data, At('path')); 8 | static value = compose(Meta.data, At('value')); 9 | static source = compose(Meta.data, At('source')); 10 | 11 | constructor(object, value) { 12 | this.root = object; 13 | this.lens = transparent; 14 | this.path = []; 15 | this.value = value; 16 | this.source = object; 17 | } 18 | } 19 | 20 | export function metaOf(object) { 21 | return view(Meta.data, object); 22 | } 23 | 24 | export function valueOf(object) { 25 | let meta = metaOf(object); 26 | return meta != null ? meta.value : object; 27 | } 28 | 29 | export function pathOf(object) { 30 | return view(Meta.path, object); 31 | } 32 | 33 | export function sourceOf(object) { 34 | return view(Meta.source, object); 35 | } 36 | 37 | export function mount(microstate, substate, key) { 38 | let parent = view(Meta.data, microstate); 39 | let prefix = compose(parent.lens, At(key, parent.value)); 40 | 41 | return over(Meta.data, meta => ({ 42 | get root() { 43 | return parent.root; 44 | }, 45 | get lens() { 46 | return compose(prefix, meta.lens); 47 | }, 48 | get path() { 49 | return parent.path.concat([key]).concat(meta.path); 50 | }, 51 | get value() { 52 | return meta.value; 53 | }, 54 | get source() { 55 | return meta.source; 56 | } 57 | }), substate); 58 | } 59 | -------------------------------------------------------------------------------- /packages/microstates/src/microstates.js: -------------------------------------------------------------------------------- 1 | import { append, stable, map } from 'funcadelic'; 2 | import { set } from './lens'; 3 | import { Meta, mount, metaOf, sourceOf, valueOf } from './meta'; 4 | import { methodsOf } from './reflection'; 5 | import dsl from './dsl'; 6 | import { Relationship, Edge, relationship } from './relationship'; 7 | import Any from './types/any'; 8 | import CachedProperty from './cached-property'; 9 | import Observable from './observable'; 10 | 11 | export function create(InputType = Any, value) { 12 | let { Type } = dsl.expand(InputType); 13 | let Microstate = MicrostateType(Type); 14 | let microstate = new Microstate(value); 15 | if (hasOwnProperty(Type.prototype, 'initialize')) { 16 | return microstate.initialize(value); 17 | } else { 18 | return microstate; 19 | } 20 | } 21 | 22 | const MicrostateType = stable(function MicrostateType(Type) { 23 | if (Type.Type) { 24 | return Type; 25 | } 26 | let Microstate = class extends Observable(Type) { 27 | static name = `Microstate<${Type.name}>`; 28 | static Type = Type; 29 | 30 | constructor(value) { 31 | super(value); 32 | Object.defineProperties(this, map((slot, key) => { 33 | let relationship = slot instanceof Relationship ? slot : legacy(slot); 34 | 35 | return CachedProperty(key, self => { 36 | let { Type, value } = relationship.traverse(new Edge(self, [key])); 37 | return mount(self, create(Type, value), key); 38 | }); 39 | }, this)); 40 | 41 | Object.defineProperty(this, Meta.symbol, { enumerable: false, configurable: true, value: new Meta(this, valueOf(value))}); 42 | } 43 | }; 44 | 45 | Object.defineProperties(Microstate.prototype, map((descriptor) => { 46 | return { 47 | value(...args) { 48 | let result = descriptor.value.apply(sourceOf(this), args); 49 | let meta = metaOf(this); 50 | let previous = valueOf(meta.root); 51 | let next = set(meta.lens, valueOf(result), previous); 52 | if (meta.path.length === 0 && metaOf(result) != null) { 53 | return result; 54 | } if (next === previous) { 55 | return meta.root; 56 | } else { 57 | return create(meta.root.constructor, next); 58 | } 59 | } 60 | }; 61 | }, append({ set: { value: x => x } }, methodsOf(Type)))); 62 | return Microstate; 63 | }); 64 | 65 | /** 66 | * Implement the legacy DSL as a relationship. 67 | * 68 | * Consider emitting a deprecation warning, as this will likely be 69 | * removed before microstates 1.0 70 | */ 71 | 72 | function legacy(object) { 73 | let cell; 74 | let meta = metaOf(object); 75 | if (meta != null) { 76 | cell = { Type: object.constructor.Type, value: valueOf(object) }; 77 | } else { 78 | cell = dsl.expand(object); 79 | } 80 | let { Type } = cell; 81 | return relationship(cell.value).map(({ value }) => ({ Type, value })); 82 | } 83 | 84 | function hasOwnProperty(target, propertyName) { 85 | return Object.prototype.hasOwnProperty.call(target, propertyName); 86 | } -------------------------------------------------------------------------------- /packages/microstates/src/observable.js: -------------------------------------------------------------------------------- 1 | import Identity from './identity'; 2 | import SymbolObservable from 'symbol-observable'; 3 | 4 | export default function Observable(Microstate) { 5 | return class extends Microstate { 6 | [SymbolObservable]() { return this['@@observable'](); } 7 | ['@@observable']() { 8 | return { 9 | subscribe: (observer) => { 10 | let next = observer.call ? observer : observer.next.bind(observer); 11 | let identity = Identity(this, next); 12 | next(identity); 13 | return identity; 14 | }, 15 | [SymbolObservable]() { 16 | return this; 17 | } 18 | }; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/microstates/src/parameterized.js: -------------------------------------------------------------------------------- 1 | import Any from './types/any'; 2 | 3 | export default function parameterized(fn) { 4 | 5 | function initialize(...args) { 6 | let Type = fn(...args); 7 | if (Type.initialize) { 8 | Type.initialize(); 9 | } 10 | return Type; 11 | } 12 | 13 | let defaultTypeParameters = new Array(fn.length); 14 | defaultTypeParameters.fill(Any); 15 | let DefaultType = initialize(...defaultTypeParameters); 16 | DefaultType.of = (...args) => initialize(...args); 17 | return DefaultType; 18 | } 19 | -------------------------------------------------------------------------------- /packages/microstates/src/pathmap.js: -------------------------------------------------------------------------------- 1 | import { methodsOf } from './reflection'; 2 | import { create } from './microstates'; 3 | import { view, Path } from './lens'; 4 | import { valueOf, Meta } from './meta'; 5 | import { defineChildren } from './tree'; 6 | import { stable } from 'funcadelic'; 7 | 8 | import Storage from './storage'; 9 | // TODO: explore compacting non-existent locations (from removed arrays and objects). 10 | 11 | export default function Pathmap(Root, ref) { 12 | let paths = new Storage(); 13 | 14 | class Location { 15 | 16 | static symbol = Symbol('Location'); 17 | 18 | static allocate(path) { 19 | let existing = paths.getPath(path.concat(Location.symbol)); 20 | if (existing) { 21 | return existing; 22 | } else { 23 | let location = new Location(path); 24 | paths.setPath(path, { [Location.symbol]: location }); 25 | return location; 26 | } 27 | } 28 | 29 | get currentValue() { 30 | return valueOf(this.microstate); 31 | } 32 | 33 | get reference() { 34 | if (!this.currentReference || (this.currentValue !== valueOf(this.currentReference))) { 35 | return this.currentReference = this.createReference(); 36 | } else { 37 | return this.currentReference; 38 | } 39 | } 40 | 41 | get microstate() { 42 | return view(this.lens, create(Root, ref.get())); 43 | } 44 | 45 | constructor(path) { 46 | this.path = path; 47 | this.lens = Path(path); 48 | this.createReferenceType = stable(Type => { 49 | let location = this; 50 | let typeName = Type.name ? Type.name: 'Unknown'; 51 | 52 | class Reference extends Type { 53 | static name = `Ref<${typeName}>`; 54 | static location = location; 55 | 56 | constructor(value) { 57 | super(value); 58 | Object.defineProperty(this, Meta.symbol, { enumerable: false, configurable: true, value: new Meta(this, valueOf(value))}); 59 | defineChildren(key => Location.allocate(path.concat(key)).reference, this); 60 | } 61 | } 62 | 63 | for (let methodName of Object.keys(methodsOf(Type)).concat("set")) { 64 | Reference.prototype[methodName] = (...args) => { 65 | let microstate = location.microstate; 66 | let next = microstate[methodName](...args); 67 | ref.set(valueOf(next)); 68 | return location.reference; 69 | }; 70 | } 71 | 72 | return Reference; 73 | }); 74 | } 75 | 76 | createReference() { 77 | let { Type } = this.microstate.constructor; 78 | let Reference = this.createReferenceType(Type); 79 | 80 | return new Reference(this.currentValue); 81 | } 82 | 83 | get(reference = paths.getPath([Location.symbol, 'reference'])) { 84 | return reference.constructor.location.reference; 85 | } 86 | } 87 | 88 | return Location.allocate([]); 89 | } 90 | -------------------------------------------------------------------------------- /packages/microstates/src/query.js: -------------------------------------------------------------------------------- 1 | export function query(iterable) { 2 | return new Query(iterable); 3 | } 4 | 5 | export function map(iterable, fn) { 6 | return query(iterable).map(fn); 7 | } 8 | 9 | export function filter(iterable, fn) { 10 | return query(iterable).filter(fn); 11 | } 12 | 13 | export function reduce(iterable, fn, initial) { 14 | return query(iterable).reduce(fn, initial); 15 | } 16 | 17 | export function find(iterable, fn){ 18 | return query(iterable).find(fn); 19 | } 20 | 21 | class Query { 22 | constructor(iterable) { 23 | if (typeof iterable[Symbol.iterator] === 'function') { 24 | this.generator = () => iterable[Symbol.iterator](); 25 | } else if (typeof iterable === 'function') { 26 | this.generator = iterable; 27 | } else { 28 | throw new Error('Query must be constructed with a generator function or iterable. Received `${iterable}`'); 29 | } 30 | } 31 | 32 | get length() { 33 | return reduce(this, sum => sum + 1, 0); 34 | } 35 | 36 | map(fn) { 37 | return query(() => { 38 | let source = this.generator(); 39 | return { 40 | next() { 41 | let next = source.next(); 42 | return { 43 | get done() { return next.done; }, 44 | get value() { return fn(next.value); } 45 | }; 46 | } 47 | }; 48 | }); 49 | } 50 | 51 | filter(fn) { 52 | return query(() => { 53 | let source = this.generator(); 54 | return { 55 | next() { 56 | let result; 57 | function find() { 58 | if (result != null) { 59 | return result; 60 | } else { 61 | // eslint-disable-next-line no-empty 62 | for (result = source.next(); !result.done && !fn(result.value); result = source.next()) {} 63 | return result; 64 | } 65 | } 66 | return { 67 | get done() { return find().done; }, 68 | get value() { return find().value; } 69 | }; 70 | } 71 | }; 72 | }); 73 | } 74 | 75 | reduce(fn, initial) { 76 | let result = initial; 77 | for (let item of this) { 78 | result = fn(result, item); 79 | } 80 | return result; 81 | } 82 | 83 | find(fn) { 84 | for (let item of this){ 85 | if (fn(item)){ 86 | return item; 87 | } 88 | } 89 | } 90 | 91 | [Symbol.iterator]() { 92 | return this.generator(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/microstates/src/reflection.js: -------------------------------------------------------------------------------- 1 | import { filter } from 'funcadelic'; 2 | 3 | const { getPrototypeOf, getOwnPropertyDescriptors, assign } = Object; 4 | 5 | export function methodsOf(Type) { 6 | return filter(({ key: name, value: desc }) => { 7 | return name !== 'constructor' && typeof name === 'string' && typeof desc.value === 'function'; 8 | }, getAllPropertyDescriptors(Type.prototype)); 9 | } 10 | 11 | /** 12 | * As opposed to `getOwnPropertyDescriptors` which only gets the 13 | * descriptors on a single object, `getAllPropertydescriptors` walks 14 | * the entire prototype chain starting at `prototype` and gather all 15 | * descriptors that are accessible to this object. 16 | */ 17 | function getAllPropertyDescriptors(object) { 18 | if (object === Object.prototype) { 19 | return {}; 20 | } else { 21 | let prototype = getPrototypeOf(object); 22 | return assign(getAllPropertyDescriptors(prototype), getOwnPropertyDescriptors(object)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/microstates/src/relationship.js: -------------------------------------------------------------------------------- 1 | import { Any } from './types'; 2 | import { view, Path } from './lens'; 3 | import { valueOf } from './meta'; 4 | 5 | export class Relationship { 6 | 7 | constructor(traverse) { 8 | this.traverse = traverse; 9 | } 10 | 11 | flatMap(sequence) { 12 | return new Relationship(edge => { 13 | let cell = this.traverse(edge); 14 | let next = sequence(cell); 15 | return next.traverse(edge); 16 | }); 17 | } 18 | 19 | map(fn) { 20 | return this.flatMap(cell => new Relationship(() => fn(cell))); 21 | } 22 | } 23 | 24 | export function relationship(definition) { 25 | if (typeof definition === 'function') { 26 | return new Relationship(definition); 27 | } else { 28 | return relationship(({ value }) => { 29 | if (value != null) { 30 | return { Type: Any, value }; 31 | } else { 32 | return { Type: Any, value: definition }; 33 | } 34 | }); 35 | } 36 | } 37 | 38 | export class Edge { 39 | constructor(parent, path) { 40 | this.parent = parent; 41 | this.path = path; 42 | } 43 | 44 | get name() { 45 | let [ name ] = this.path.slice(-1); 46 | return name; 47 | } 48 | 49 | get value() { 50 | return view(Path(this.path), this.parentValue); 51 | } 52 | 53 | get parentValue() { 54 | return valueOf(this.parent); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/microstates/src/storage.js: -------------------------------------------------------------------------------- 1 | import { view, set, Path } from './lens'; 2 | 3 | export default class Storage { 4 | constructor(value, observe = x => x) { 5 | this.value = value; 6 | this.observe = observe; 7 | } 8 | 9 | get() { 10 | return this.value; 11 | } 12 | 13 | set(value) { 14 | if (value !== this.value) { 15 | this.value = value; 16 | this.observe(); 17 | } 18 | return this; 19 | } 20 | 21 | getPath(path) { 22 | return view(Path(path), this.value); 23 | } 24 | 25 | setPath(path, value) { 26 | return this.set(set(Path(path), value, this.value)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/microstates/src/tree.js: -------------------------------------------------------------------------------- 1 | import { type } from 'funcadelic'; 2 | 3 | import CachedProperty from './cached-property'; 4 | 5 | export const Tree = type(class { 6 | static name = 'Tree'; 7 | 8 | childAt(key, parent) { 9 | if (parent[Tree.symbol]) { 10 | return this(parent).childAt(key, parent); 11 | } else { 12 | return parent[key]; 13 | } 14 | } 15 | 16 | defineChildren(fn, parent) { 17 | if (parent[Tree.symbol]) { 18 | return this(parent).defineChildren(fn, parent); 19 | } else { 20 | for (let property of Object.keys(parent)) { 21 | Object.defineProperty(parent, property, CachedProperty(property, () => fn(property, parent))); 22 | } 23 | } 24 | } 25 | }); 26 | 27 | export const { childAt, defineChildren } = Tree.prototype; 28 | -------------------------------------------------------------------------------- /packages/microstates/src/types.js: -------------------------------------------------------------------------------- 1 | import Any from './types/any'; 2 | import ObjectType from './types/object'; 3 | import ArrayType from './types/array'; 4 | import BooleanType from './types/boolean'; 5 | import NumberType from './types/number'; 6 | import StringType from './types/string'; 7 | import Primitive from './types/primitive'; 8 | 9 | export { Any, ObjectType, ArrayType, BooleanType, NumberType, StringType, Primitive }; 10 | -------------------------------------------------------------------------------- /packages/microstates/src/types/any.js: -------------------------------------------------------------------------------- 1 | import Primitive from './primitive'; 2 | 3 | export default class Any extends Primitive {} 4 | -------------------------------------------------------------------------------- /packages/microstates/src/types/array.js: -------------------------------------------------------------------------------- 1 | import { At, set } from '../lens'; 2 | import { mount, valueOf } from '../meta'; 3 | import { create } from '../microstates'; 4 | import parameterized from '../parameterized'; 5 | import { Tree, childAt } from '../tree'; 6 | 7 | export default parameterized(T => class ArrayType { 8 | static T = T; 9 | 10 | static get name() { 11 | return `Array<${T.name}>`; 12 | } 13 | 14 | get length() { 15 | return valueOf(this).length; 16 | } 17 | 18 | initialize(value) { 19 | if (value == null) { 20 | return []; 21 | } else if (Array.isArray(value) || value[Symbol.iterator]) { 22 | return value; 23 | } else { 24 | return [value]; 25 | } 26 | } 27 | 28 | push(value) { 29 | return [...valueOf(this), valueOf(value)]; 30 | } 31 | 32 | pop() { 33 | return valueOf(this).slice(0, -1); 34 | } 35 | 36 | shift() { 37 | let [, ...rest] = valueOf(this); 38 | return rest; 39 | } 40 | 41 | unshift(value) { 42 | return [valueOf(value), ...valueOf(this)]; 43 | } 44 | 45 | slice(begin, end) { 46 | let list = valueOf(this); 47 | let result = list.slice(begin, end); 48 | return list.length === result.length ? this : result; 49 | } 50 | 51 | sort(compareFn) { 52 | let init = valueOf(this); 53 | let result = [...init].sort(compareFn); 54 | return init.every((member, idx) => result[idx] === member) ? this : result; 55 | } 56 | 57 | filter(fn) { 58 | let list = valueOf(this); 59 | let result = list.filter((member) => fn(create(T, member))); 60 | return list.length === result.length ? this : result; 61 | } 62 | 63 | map(fn) { 64 | let list = valueOf(this); 65 | return list.reduce((result, member, index) => { 66 | let mapped = valueOf(fn(create(T, member))); 67 | return set(At(index, result), mapped, result); 68 | }, list); 69 | } 70 | 71 | remove(item) { 72 | return this.filter(s => valueOf(s) !== valueOf(item)); 73 | } 74 | 75 | clear() { 76 | return []; 77 | } 78 | 79 | [Symbol.iterator]() { 80 | let array = this; 81 | let iterator = valueOf(this)[Symbol.iterator](); 82 | let i = 0; 83 | return { 84 | next() { 85 | let next = iterator.next(); 86 | let index = i++; 87 | return { 88 | get done() { return next.done; }, 89 | get value() { 90 | if (!next.done) { 91 | return childAt(index, array); 92 | } else { 93 | return undefined; 94 | } 95 | } 96 | }; 97 | } 98 | }; 99 | } 100 | 101 | static initialize() { 102 | 103 | Tree.instance(this, { 104 | childAt(key, array) { 105 | if (typeof key === 'number') { 106 | let value = valueOf(array)[key]; 107 | return mount(array, create(T, value), key); 108 | } else { 109 | return array[key]; 110 | } 111 | }, 112 | 113 | defineChildren(fn, array) { 114 | let generate = array[Symbol.iterator]; 115 | return Object.defineProperty(array, Symbol.iterator, { 116 | enumerable: false, 117 | value() { 118 | let iterator = generate.call(array); 119 | let i = 0; 120 | return { 121 | next() { 122 | let next = iterator.next(); 123 | let index = i++; 124 | return { 125 | get done() { return next.done; }, 126 | get value() { 127 | if (!next.done) { 128 | return fn(index, next.value, array); 129 | } else { 130 | return undefined; 131 | } 132 | } 133 | }; 134 | } 135 | }; 136 | } 137 | }); 138 | } 139 | }); 140 | } 141 | }); 142 | -------------------------------------------------------------------------------- /packages/microstates/src/types/boolean.js: -------------------------------------------------------------------------------- 1 | import Primtive from './primitive'; 2 | 3 | export default class BooleanType extends Primtive { 4 | static name = "Boolean"; 5 | 6 | initialize(value) { 7 | return !!value; 8 | } 9 | toggle() { 10 | return !this.state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/microstates/src/types/number.js: -------------------------------------------------------------------------------- 1 | import Primitive from './primitive'; 2 | 3 | export default class NumberType extends Primitive { 4 | static name = "Number"; 5 | 6 | initialize(value) { 7 | if (value == null) { 8 | return 0; 9 | } else if (isNaN(value)) { 10 | return this; 11 | } else { 12 | return Number(value); 13 | } 14 | } 15 | increment(step = 1) { 16 | return this.state + step; 17 | } 18 | 19 | decrement(step = 1) { 20 | return this.state - step; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/microstates/src/types/object.js: -------------------------------------------------------------------------------- 1 | import { append, filter, map } from 'funcadelic'; 2 | import { reduce, query } from '../query'; 3 | import parameterized from '../parameterized'; 4 | import { valueOf, mount } from '../meta'; 5 | import { create } from '../microstates'; 6 | import { Tree, childAt } from '../tree'; 7 | 8 | 9 | export default parameterized(T => class ObjectType { 10 | static T = T; 11 | 12 | static name = `Object<${T.name}>` 13 | 14 | get entries() { 15 | return reduce(this, (entries, entry) => Object.assign(entries, { 16 | [entry.key]: entry.value 17 | }), {}); 18 | } 19 | 20 | get keys() { 21 | return Object.keys(valueOf(this)); 22 | } 23 | 24 | get values() { 25 | return query(this).map(entry => entry.value); 26 | } 27 | 28 | initialize(value) { 29 | return value == null ? {} : this; 30 | } 31 | 32 | assign(attrs) { 33 | return append(valueOf(this), map(valueOf, attrs)); 34 | } 35 | 36 | put(name, value) { 37 | return this.assign({[name]: value}); 38 | } 39 | 40 | delete(name) { 41 | return filter(({ key }) => key !== name, valueOf(this)); 42 | } 43 | 44 | map(fn) { 45 | return map(value => valueOf(fn(create(T, value))), valueOf(this)); 46 | } 47 | 48 | filter(fn) { 49 | return filter(({ key, value }) => valueOf(fn(create(T, value), key)), valueOf(this)); 50 | } 51 | 52 | [Symbol.iterator]() { 53 | let object = this; 54 | let iterator = Object.keys(valueOf(this))[Symbol.iterator](); 55 | return { 56 | next() { 57 | let next = iterator.next(); 58 | return { 59 | get done() { return next.done; }, 60 | get value() { 61 | if (!next.done) { 62 | return new Entry(next.value, childAt(next.value, object)); 63 | } else { 64 | return undefined; 65 | } 66 | } 67 | }; 68 | } 69 | }; 70 | } 71 | 72 | static initialize() { 73 | Tree.instance(this, { 74 | childAt(key, object) { 75 | if (typeof key !== 'string') { 76 | return object[key]; 77 | } else { 78 | let value = valueOf(object)[key]; 79 | return mount(object, create(T, value), key); 80 | } 81 | }, 82 | defineChildren(fn, object) { 83 | let generate = object[Symbol.iterator]; 84 | return Object.defineProperty(object, Symbol.iterator, { 85 | enumerable: false, 86 | value() { 87 | let iterator = generate.call(object); 88 | return { 89 | next() { 90 | let next = iterator.next(); 91 | return { 92 | get done() { return next.done; }, 93 | get value() { 94 | if (!next.done) { 95 | let { key } = next.value; 96 | return new Entry(key, fn(key)); 97 | } else { 98 | return undefined; 99 | } 100 | } 101 | }; 102 | } 103 | }; 104 | } 105 | }); 106 | } 107 | }); 108 | } 109 | }); 110 | 111 | class Entry { 112 | constructor(key, value) { 113 | this.key = key; 114 | this.value = value; 115 | } 116 | 117 | [Symbol.iterator]() { 118 | return [this.value, this.key][Symbol.iterator](); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/microstates/src/types/primitive.js: -------------------------------------------------------------------------------- 1 | import { valueOf } from '../meta'; 2 | 3 | export default class Primitive { 4 | get state() { 5 | return valueOf(this); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/microstates/src/types/string.js: -------------------------------------------------------------------------------- 1 | import Primitive from './primitive'; 2 | 3 | export default class StringType extends Primitive { 4 | static name = "String"; 5 | 6 | initialize(value) { 7 | if (value == null) { 8 | return ''; 9 | } else { 10 | return String(value); 11 | } 12 | } 13 | 14 | concat(value) { 15 | return this.state.concat(value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/microstates/tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prefer-let"], 3 | "rules": { 4 | "no-return-assign": 0, 5 | "no-unused-expressions": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/microstates/tests/constants.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../src/microstates'; 4 | 5 | let o = { hello: 'world' }; 6 | class Type { 7 | n = 10; 8 | b = true; 9 | s = 'hello'; 10 | o = o; 11 | a = ['a', 'b', 'c']; 12 | null = null; 13 | greeting = String; 14 | } 15 | 16 | describe('constants support', () => { 17 | let ms, next; 18 | beforeEach(() => { 19 | ms = create(Type, {}); 20 | next = ms.greeting.set('HI'); 21 | }); 22 | 23 | it('includes constants in state tree', () => { 24 | expect(ms.n.state).toEqual(10); 25 | expect(ms.b.state).toEqual(true); 26 | expect(ms.s.state).toEqual('hello'); 27 | expect(ms.o.state).toEqual({hello: 'world'}); 28 | expect(ms.a.state).toEqual(['a', 'b', 'c']); 29 | expect(ms.null.state).toEqual(null); 30 | expect(ms.greeting.state).toEqual(''); 31 | }); 32 | 33 | it('next state has constants', () => { 34 | expect(next.n.state).toEqual(10); 35 | expect(next.b.state).toEqual(true); 36 | expect(next.s.state).toEqual('hello'); 37 | expect(next.o.state).toEqual({hello: 'world'}); 38 | expect(next.a.state).toEqual(['a', 'b', 'c']); 39 | expect(next.null.state).toEqual(null); 40 | expect(next.greeting.state).toEqual('HI'); 41 | }); 42 | 43 | it('next valueOf excludes constants', () => { 44 | expect(next.greeting.state).toEqual('HI'); 45 | }); 46 | 47 | it('shares complex objects between multiple instances of microstate', () => { 48 | expect(ms.o.state).toBe(create(Type, {}).o.state); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/microstates/tests/dsl.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | import expect from 'expect'; 3 | import dsl from '../src/dsl'; 4 | import { ObjectType, ArrayType, NumberType, StringType, BooleanType } from '../src/types'; 5 | 6 | describe('DSL', () => { 7 | it('expands bare Object', function() { 8 | let expansion = dsl.expand(Object); 9 | expect(expansion.Type).toEqual(ObjectType); 10 | expect(expansion.value).toEqual({}); 11 | }); 12 | 13 | it('expands bare Array', function() { 14 | let expansion = dsl.expand(Array); 15 | expect(expansion.Type).toBe(ArrayType); 16 | expect(expansion.value).toEqual([]); 17 | }); 18 | 19 | it('expands bare Number', function() { 20 | let expansion = dsl.expand(Number); 21 | expect(expansion.Type).toBe(NumberType); 22 | expect(expansion.value).toEqual(0); 23 | }); 24 | 25 | it('expands bare Boolean', function() { 26 | let expansion = dsl.expand(Boolean); 27 | expect(expansion.Type).toBe(BooleanType); 28 | expect(expansion.value).toEqual(false); 29 | }); 30 | 31 | it('expands bare String', function() { 32 | let expansion = dsl.expand(String); 33 | expect(expansion.Type).toBe(StringType); 34 | expect(expansion.value).toEqual(''); 35 | }); 36 | 37 | it('expands [] as a Array', function() { 38 | let expansion = dsl.expand([]); 39 | expect(expansion.Type).toBe(ArrayType); 40 | expect(expansion.value).toEqual([]); 41 | }); 42 | 43 | it('expands [Number] as an Array parameterized by Number', function() { 44 | let expansion = dsl.expand([Number]); 45 | expect(expansion.Type.name).toEqual("Array"); 46 | expect(expansion.Type.T).toEqual(NumberType); 47 | expect(expansion.value).toEqual([]); 48 | }); 49 | 50 | it('expands {} as an Object', function() { 51 | let { Type, value } = dsl.expand({}); 52 | expect(Type).toBe(ObjectType); 53 | expect(value).toEqual({}); 54 | }); 55 | 56 | it('expands {Number} as an Object parameterized by Number', function() { 57 | let { Type, value } = dsl.expand({ Number }); 58 | expect(Type.name).toEqual("Object"); 59 | expect(Type.T).toEqual(NumberType); 60 | expect(value).toEqual({}); 61 | }); 62 | 63 | it('recursively expands container types', function() { 64 | let { Type, value } = dsl.expand([{Class: [Number]}]); 65 | expect(Type.name).toEqual("Array>>"); 66 | expect(value).toEqual([]); 67 | }); 68 | 69 | it('expands constants as consntant', function() { 70 | let { Type, value } = dsl.expand(5); 71 | expect(Type.name).toEqual("Constant"); 72 | expect(value).toEqual(5); 73 | }); 74 | 75 | it('matches constructors to themselves', function() { 76 | class MyType {} 77 | let { Type, value } = dsl.expand(MyType); 78 | expect(Type).toBe(MyType); 79 | expect(value).toBeUndefined(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /packages/microstates/tests/examples/authentication.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../../src/microstates'; 4 | 5 | class AnonymousSession { 6 | initialize(session) { 7 | if (session) { 8 | return this.authenticate(session); 9 | } 10 | return this; 11 | } 12 | authenticate(session) { 13 | return create(AuthenticatedSession, session); 14 | } 15 | } 16 | 17 | class AuthenticatedSession { 18 | isAuthenticated = create(Boolean, true); 19 | content = create(Object, {}); 20 | 21 | logout() { 22 | return create(AnonymousSession); 23 | } 24 | } 25 | 26 | class MyApp { 27 | session = create(AnonymousSession); 28 | } 29 | 30 | describe('AnonymousSession', () => { 31 | let ms; 32 | beforeEach(() => { 33 | ms = create(MyApp); 34 | }); 35 | 36 | it('initializes into AnonymousSession without initial state', () => { 37 | expect(ms.session).toBeInstanceOf(AnonymousSession); 38 | }); 39 | 40 | describe('transition', () => { 41 | let authenticated; 42 | beforeEach(() => { 43 | authenticated = ms.session.authenticate({ 44 | name: 'Charles', 45 | }); 46 | }); 47 | 48 | it('transitions AnonymousSession to Authenticated with authenticate', () => { 49 | expect(authenticated.session).toBeInstanceOf(AuthenticatedSession); 50 | // expect(authenticated.session.content.name.state).toEqual('Charles'); 51 | // expect(authenticated.session.isAuthenticated.state).toEqual(true); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('AuthenticatedSession', () => { 57 | let ms, anonymous; 58 | beforeEach(() => { 59 | ms = create(MyApp, { session: { name: 'Taras', isAuthenticated: true } }); 60 | anonymous = ms.session.logout(); 61 | }); 62 | 63 | it('initializes into AuthenticatedSession state', () => { 64 | expect(ms.session).toBeInstanceOf(AuthenticatedSession); 65 | }); 66 | 67 | it('transitions Authenticated session to AnonymousSession with logout', () => { 68 | expect(anonymous.session).toBeInstanceOf(AnonymousSession); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/microstates/tests/examples/cart.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | 4 | import { create } from '../../src/microstates'; 5 | import { valueOf } from '../../src/meta'; 6 | import { reduce, map } from '../../src/query'; 7 | 8 | describe('cart example', () => { 9 | class Cart { 10 | products = create(Array); 11 | 12 | get price() { 13 | return reduce(this.products, (acc, product) => acc + product.state.quantity * product.state.price, 0); 14 | } 15 | 16 | get count() { 17 | return reduce(this.products, (acc, product) => acc + product.state.quantity, 0); 18 | } 19 | 20 | get prices() { 21 | return map(this.products, product => product.state.price); 22 | } 23 | 24 | } 25 | 26 | describe('adding products without initial value', () => { 27 | let ms; 28 | beforeEach(() => { 29 | ms = create(Cart, { products: [] }) 30 | .products.push({ quantity: 1, price: 10 }) 31 | .products.push({ quantity: 2, price: 20 }); 32 | }); 33 | 34 | it('adds items to the cart', () => { 35 | expect(ms.price).toEqual(50); 36 | expect(ms.count).toEqual(3); 37 | expect(valueOf(ms)).toMatchObject({ 38 | products: [{ quantity: 1, price: 10 }, { quantity: 2, price: 20 }], 39 | }); 40 | }); 41 | 42 | it('provides state', () => { 43 | expect(valueOf(ms)).toEqual({ 44 | products: [{ quantity: 1, price: 10 }, { quantity: 2, price: 20 }], 45 | }); 46 | }); 47 | 48 | it('maps products', () => { 49 | let [...prices] = ms.prices; 50 | expect(prices).toEqual([10, 20]); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/microstates/tests/identity.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | 4 | import Identity from '../src/identity'; 5 | import { create } from '../src/microstates'; 6 | import { valueOf } from '../src/meta'; 7 | import from from '../src/literal'; 8 | 9 | import { TodoMVC, Todo } from './todomvc'; 10 | 11 | describe('Identity', () => { 12 | 13 | describe('complex type', () => { 14 | let id; 15 | let microstate; 16 | let latest; 17 | beforeEach(function() { 18 | microstate = create(TodoMVC) 19 | .todos.push({ title: "Take out The Milk", completed: true }) 20 | .todos.push({ title: "Convince People Microstates is awesome" }) 21 | .todos.push({ title: "Take out the Trash" }) 22 | .todos.push({ title: "profit $$"}); 23 | latest = id = Identity(microstate, x => latest = x); 24 | }); 25 | 26 | it('is derived from its source object', function() { 27 | expect(id).toBeInstanceOf(TodoMVC); 28 | }); 29 | 30 | it('has the same shape as the initial state.', function() { 31 | expect(id.completeAll).toBeInstanceOf(Function); 32 | expect(id.todos).toHaveLength(4); 33 | 34 | let [ first ] = id.todos; 35 | let [ $first ] = microstate.todos; 36 | expect(first).toBeInstanceOf(Todo); 37 | expect(valueOf(first)).toBe(valueOf($first)); 38 | }); 39 | 40 | describe('invoking a transition', function() { 41 | let third; 42 | beforeEach(function() { 43 | [ ,, third ] = id.todos; 44 | 45 | third.completed.set(true); 46 | }); 47 | 48 | it('transitions the nodes which did change', function() { 49 | expect(latest).not.toBe(id); 50 | expect(latest.todos).not.toBe(id.todos); 51 | let [ ,, $third] = latest.todos; 52 | expect($third).not.toBe(third); 53 | }); 54 | 55 | it('maintains the === identity of the nodes which did not change', function() { 56 | let [first, second, third, fourth] = id.todos; 57 | let [$first, $second, $third, $fourth] = latest.todos; 58 | expect($third.title).toBe(third.title); 59 | expect($first).toBe(first); 60 | expect($second).toBe(second); 61 | expect($fourth).toBe(fourth); 62 | }); 63 | }); 64 | 65 | describe('implicit method binding', function() { 66 | beforeEach(function() { 67 | let shift = id.todos.shift; 68 | shift(); 69 | }); 70 | 71 | it('still completes the transition', function() { 72 | expect(valueOf(latest)).toEqual({ 73 | todos: [{ 74 | title: "Convince People Microstates is awesome" 75 | }, { 76 | title: "Take out the Trash" 77 | }, { 78 | title: "profit $$" 79 | }] 80 | }); 81 | }); 82 | }); 83 | 84 | describe('transition stability', function() { 85 | beforeEach(function() { 86 | let [ first ] = id.todos; 87 | first.completed.set(false); 88 | }); 89 | 90 | it('uses the same function for each location in the graph, even for different instances', function() { 91 | expect(latest).not.toBe(id); 92 | expect(latest.set).toBe(id.set); 93 | 94 | let [ first ] = id.todos; 95 | let [ $first ] = latest.todos; 96 | 97 | expect($first.push).toBe(first.push); 98 | expect($first.completed.toggle).toBe(first.completed.toggle); 99 | }); 100 | }); 101 | 102 | describe('the identity callback function', function() { 103 | let store; 104 | beforeEach(function() { 105 | store = Identity(microstate, () => undefined); 106 | }); 107 | 108 | it('ignores the return value of the callback function when determining the value of the store', function() { 109 | expect(store).toBeDefined(); 110 | expect(store).toBeInstanceOf(TodoMVC); 111 | }); 112 | }); 113 | 114 | describe('idempotency', function() { 115 | let calls; 116 | let store; 117 | beforeEach(function() { 118 | calls = 0; 119 | store = Identity(microstate, x => { 120 | calls++; 121 | return x; 122 | }); 123 | let [ first ] = store.todos; 124 | first.completed.set(true); 125 | }); 126 | 127 | it('does not invoke the idenity function on initial invocation', function() { 128 | expect(calls).toEqual(0); 129 | }); 130 | }); 131 | 132 | describe('identity of queries', function() { 133 | it('traverses queries and includes the microstates within them', function() { 134 | expect(id.completed).toBeDefined(); 135 | let [ firstCompleted ] = id.completed; 136 | expect(firstCompleted).toBeInstanceOf(Todo); 137 | }); 138 | 139 | describe('the effect of transitions on query identities', () => { 140 | let first, second; 141 | beforeEach(function() { 142 | [ first, second ] = id.completed; 143 | first.title.set('Take out the milk'); 144 | }); 145 | 146 | it('updates those queries which contain changed objects, but not ids *within* the query that remained the same', () => { 147 | let [$first, $second] = latest.completed; 148 | expect(latest.completed).not.toBe(id.completed); 149 | expect($first).not.toBe(first); 150 | expect($second).toBe(second); 151 | }); 152 | 153 | it.skip('maintains the === identity of those queries which did not change', function() { 154 | let [first, second] = id.active; 155 | let [$first, $second] = latest.active; 156 | expect($first).toBe(first); 157 | expect($second).toBe(second); 158 | expect(latest.active).toBe(id.active); 159 | }); 160 | 161 | it('maintains the === identity of the same node that appears at different spots in the tree', () => { 162 | let [ first ] = id.todos; 163 | let [ firstCompleted ] = id.completed; 164 | let [ $first ] = latest.todos; 165 | let [ $firstCompleted ] = latest.completed; 166 | expect(first).toBe(firstCompleted); 167 | expect($first).toBe($firstCompleted); 168 | }); 169 | }); 170 | }); 171 | }); 172 | 173 | describe('identity support of type reuse', () => { 174 | class Person { 175 | name = String; 176 | father = Person; 177 | mother = Person; 178 | } 179 | 180 | it('it keeps both branches', () => { 181 | let s = Identity(create(Person), latest => (s = latest)); 182 | let { name, father, mother } = s; 183 | 184 | name.set('Bart'); 185 | father.name.set('Homer'); 186 | mother.name.set('Marge'); 187 | 188 | expect({ 189 | name: s.name.state, 190 | father: { name: s.father.name.state }, 191 | mother: { name: s.mother.name.state } 192 | }).toEqual({ 193 | name: 'Bart', 194 | father: { name: 'Homer' }, 195 | mother: { name: 'Marge' } 196 | }); 197 | }); 198 | }); 199 | 200 | describe('identity support for microstates created with from(object)', () => { 201 | let i; 202 | beforeEach(() => { 203 | i = Identity(from({ name: 'Taras' }), latest => i = latest); 204 | i.entries.name.concat('!!!'); 205 | }); 206 | it('allows to transition value of a property', () => { 207 | expect(i.entries.name.state).toBe('Taras!!!'); 208 | }); 209 | }); 210 | 211 | describe('supports destructuring inherited transitions', () => { 212 | class Parent { 213 | a = String; 214 | setA(str) { 215 | return this.a.set(str); 216 | } 217 | } 218 | class Child extends Parent {} 219 | 220 | let i, setA; 221 | beforeEach(() => { 222 | i = Identity(create(Child), latest => i = latest); 223 | setA = i.setA; 224 | }); 225 | 226 | it('allows invoking a transition', () => { 227 | setA('taras'); 228 | expect(i.a.state).toEqual('taras'); 229 | }); 230 | }); 231 | }); 232 | -------------------------------------------------------------------------------- /packages/microstates/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | import expect from 'expect'; 3 | 4 | import * as index from '../index'; 5 | 6 | describe('index of module exports', () => { 7 | it('has a create function', () => { 8 | expect(index.create).toBeInstanceOf(Function); 9 | }); 10 | it('has a from function for instatiating literals', () => { 11 | expect(index.from).toBeInstanceOf(Function); 12 | }); 13 | it('has map, filter, reduce, and find queries', () => { 14 | expect(index.filter).toBeInstanceOf(Function); 15 | expect(index.map).toBeInstanceOf(Function); 16 | expect(index.reduce).toBeInstanceOf(Function); 17 | expect(index.find).toBeInstanceOf(Function); 18 | }); 19 | it('has Store constructor', () => { 20 | expect(index.Store).toBeInstanceOf(Function); 21 | }); 22 | it('has metaOf() and valueOf() for opening microstate boxes', () => { 23 | expect(index.metaOf).toBeInstanceOf(Function); 24 | expect(index.valueOf).toBeInstanceOf(Function); 25 | }); 26 | it('has the relationship function', () => { 27 | expect(index.relationship).toBeInstanceOf(Function); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/microstates/tests/initialization.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | 4 | import { create } from '../src/microstates'; 5 | import { valueOf } from '../src/meta'; 6 | 7 | describe('initialization', () => { 8 | describe('at root', () => { 9 | class Session { 10 | initialize(data = { token: null }) { 11 | if (data.token) { 12 | return create(Authenticated, data); 13 | } 14 | return create(Anonymous, data); 15 | } 16 | } 17 | class Authenticated extends Session { 18 | token = create(class StringType {}); 19 | logout() {} 20 | } 21 | class Anonymous extends Session { 22 | signin() {} 23 | } 24 | 25 | describe('initialize without token', () => { 26 | let initialized; 27 | beforeEach(() => { 28 | initialized = create(Session); 29 | }); 30 | 31 | it('initilizes into another type', () => { 32 | expect(initialized).toBeInstanceOf(Anonymous); 33 | }); 34 | 35 | it('has signin transition', () => { 36 | expect(initialized.signin).toBeInstanceOf(Function); 37 | }); 38 | 39 | describe('calling initialize on initialized microstate', () => { 40 | let reinitialized; 41 | beforeEach(() => { 42 | reinitialized = initialized.initialize({ token: 'foo' }); 43 | }); 44 | 45 | it('initilizes into Authenticated', () => { 46 | expect(reinitialized).toBeInstanceOf(Authenticated); 47 | expect(valueOf(reinitialized)).toEqual({ token: 'foo' }); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('initialize with token', () => { 53 | let initialized; 54 | beforeEach(() => { 55 | initialized = create(Session, { token: 'SECRET' }); 56 | }); 57 | 58 | it('initilizes into Authenticated', () => { 59 | expect(initialized).toBeInstanceOf(Authenticated); 60 | expect(valueOf(initialized)).toEqual({ token: 'SECRET' }); 61 | }); 62 | 63 | it('has signin transition', () => { 64 | expect(initialized.logout).toBeInstanceOf(Function); 65 | }); 66 | }); 67 | }); 68 | 69 | describe("deeply nested", () => { 70 | class Root { 71 | first = First; 72 | } 73 | class First { 74 | second = Second; 75 | } 76 | class Second { 77 | name = String; 78 | 79 | initialize(props) { 80 | if (!props) { 81 | return create(Second, { name: "default" }); 82 | } 83 | return this; 84 | } 85 | } 86 | 87 | describe('initialization', () => { 88 | let root; 89 | beforeEach(() => { 90 | root = create(Root, { first: { } }); 91 | }); 92 | 93 | describe('transition', () => { 94 | let changed; 95 | beforeEach(() => { 96 | changed = root.first.second.name.concat("!!!"); 97 | }); 98 | 99 | it("has result after transition valueOf", () => { 100 | expect(changed.first.second.name.state).toEqual("default!!!"); 101 | }); 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /packages/microstates/tests/lens.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { compose, view, set, At } from '../src/lens'; 4 | 5 | describe('At', function() { 6 | let lens; 7 | beforeEach(function() { 8 | lens = compose(At(0, []), At("hello")); 9 | }); 10 | 11 | it('instantiates objects of the correct type at each path', function() { 12 | expect(set(lens, "world", undefined)).toEqual([{hello: 'world'}]); 13 | }); 14 | 15 | it('set-get: view retrievs what set put in', function() { 16 | let cookie = {}; 17 | let object = set(lens, cookie, undefined); 18 | expect(view(lens, object)).toBe(cookie); 19 | }); 20 | 21 | it('get-set: If you set focus to the same value it has, the whole does not change', function() { 22 | let object = set(lens, "world", undefined); 23 | expect(set(lens, "world", object)).toBe(object); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/microstates/tests/literal.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | import expect from 'expect'; 3 | import literal from '../src/literal'; 4 | import { valueOf } from '../src/meta'; 5 | 6 | describe('Literal Syntax', function() { 7 | it('can create numbers', function() { 8 | expect(literal(5).increment().state).toEqual(6); 9 | }); 10 | 11 | it('can create strings', function() { 12 | expect(literal('hello').concat(' goodbye').state).toEqual('hello goodbye'); 13 | }); 14 | 15 | it('can create booleans', function() { 16 | expect(literal(true).toggle().state).toEqual(false); 17 | }); 18 | 19 | it('can create objects', function() { 20 | expect(valueOf(literal({}).put('hello', 'world'))).toEqual({hello: 'world'}); 21 | }); 22 | 23 | it('can create nulls', function() { 24 | expect(valueOf(literal(null))).toEqual(null); 25 | }); 26 | 27 | it('can create arrays', function() { 28 | expect(valueOf(literal([]).push('hello').push('world'))).toEqual(['hello', 'world']); 29 | }); 30 | 31 | it('understands deeply nested objects and arrays', function() { 32 | let blob = literal({array: [5, { bool: true }], string: "hi", object: {object: {}}}); 33 | let [ first] = blob.entries.array; 34 | let [ _, second ] = first.increment().entries.array; // eslint-disable-line no-unused-vars 35 | 36 | let ms = second.entries.bool.toggle() 37 | .entries.string.concat(" mom") 38 | .entries.object.put('another', 'property') 39 | .entries.object.entries.object.put('deep', 'state'); 40 | 41 | expect(valueOf(ms)).toEqual({ 42 | array: [6, { bool: false }], 43 | string: "hi mom", 44 | object: { 45 | another: 'property', 46 | object: { 47 | deep: 'state' 48 | } 49 | } 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/microstates/tests/microstates.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../src/microstates'; 4 | import { valueOf } from '../src/meta'; 5 | 6 | import Any from '../src/types/any'; 7 | 8 | describe("Microstates", () => { 9 | describe("default", () => { 10 | let def; 11 | beforeEach(function() { 12 | def = create(); 13 | }); 14 | 15 | it('is an instance of Any', function() { 16 | expect(def).toBeInstanceOf(Any); 17 | }); 18 | 19 | it('has an undefined state', function() { 20 | expect(def.state).toBeUndefined(); 21 | }); 22 | }); 23 | 24 | describe('Any', function() { 25 | let any; 26 | beforeEach(function() { 27 | any = create(Any, 'ohai'); 28 | }); 29 | 30 | it('has the value as its state', function() { 31 | expect(any.state).toBe('ohai'); 32 | }); 33 | 34 | describe('setting the value to a different value', function() { 35 | let next; 36 | beforeEach(function() { 37 | next = any.set('ohai there'); 38 | }); 39 | 40 | it('returns a new object', function() { 41 | expect(next).not.toBe(any); 42 | }); 43 | 44 | it('has the new value as its state', function() { 45 | expect(next.state).toBe('ohai there'); 46 | }); 47 | }); 48 | 49 | describe('setting the value to the same value', function() { 50 | let next; 51 | beforeEach(function() { 52 | next = any.set('ohai'); 53 | }); 54 | 55 | it('returns the same object', function() { 56 | expect(next).toBe(any); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('Something containing something else', function() { 62 | class Something { 63 | any = create(Any, "ohai"); 64 | } 65 | 66 | describe('when created with something else', function() { 67 | let something; 68 | beforeEach(function() { 69 | something = create(Something, { any: "ohai there" }); 70 | }); 71 | 72 | it('properly initializes substates', function() { 73 | expect(something.any.state).toBe("ohai there"); 74 | }); 75 | 76 | describe('setting the nested object', function() { 77 | let next; 78 | beforeEach(function() { 79 | next = something.any.set("ohai boss"); 80 | }); 81 | 82 | it('returns a new instance of the something', function() { 83 | expect(next).toBeInstanceOf(Something); 84 | }); 85 | 86 | it('contains a fully realized state with the update', function() { 87 | expect(next.any.state).toEqual("ohai boss"); 88 | }); 89 | 90 | it('updates the state of the nested object', function() { 91 | expect(next.any.state).toBe('ohai boss'); 92 | }); 93 | }); 94 | 95 | describe('setting the nested object to the same value', function() { 96 | let next; 97 | beforeEach(function() { 98 | next = something.any.set(5).any.set(5); 99 | }); 100 | 101 | it('maintains its shape', function() { 102 | expect(next.any.state).toEqual(5); 103 | }); 104 | 105 | it('preserves its === equivalence', function() { 106 | expect(next.any.set(5).any.set(5)).toBe(next); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | describe('Nested Microstates with Custom transitions', function() { 113 | class Modal { 114 | isOpen = create(Boolean, false); 115 | 116 | show() { 117 | return this.isOpen.set(true); 118 | } 119 | 120 | hide() { 121 | return this.isOpen.set(false); 122 | } 123 | } 124 | 125 | class App { 126 | error = create(Modal, { isOpen: false }) 127 | warning = create(Modal, { isOpen: false}) 128 | 129 | openAll() { 130 | return this 131 | .error.show() 132 | .warning.show(); 133 | } 134 | } 135 | 136 | let modal; 137 | beforeEach(function() { 138 | modal = create(Modal, { isOpen: false }); 139 | }); 140 | 141 | it('can transition', function() { 142 | let next = modal.show(); 143 | expect(next).toBeInstanceOf(Modal); 144 | expect(next.isOpen.state).toEqual(true); 145 | }); 146 | 147 | describe('nested within nested transitions', function() { 148 | let app; 149 | beforeEach(function() { 150 | app = create(App).openAll(); 151 | }); 152 | 153 | it('returns an App', function() { 154 | expect(app).toBeInstanceOf(App); 155 | }); 156 | 157 | it('has the right state', function() { 158 | expect(valueOf(app)).toEqual({ error: { isOpen: true }, warning: { isOpen: true }}); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /packages/microstates/tests/mount.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../src/microstates'; 4 | import { mount, valueOf, pathOf } from '../src/meta'; 5 | 6 | describe('mounting', function() { 7 | class Node {} 8 | let parent, child, mounted; 9 | 10 | beforeEach(function() { 11 | parent = create(Node); 12 | child = create(Node); 13 | mounted = mount(parent, child, 'child'); 14 | }); 15 | 16 | it('has the right path', function() { 17 | expect(pathOf(mounted)).toEqual(['child']); 18 | }); 19 | 20 | describe('transitioning', function() { 21 | let next; 22 | beforeEach(function() { 23 | next = mounted.set('next value'); 24 | }); 25 | 26 | it('returns a parent', function() { 27 | expect(valueOf(next)).toEqual({child: 'next value'}); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/microstates/tests/observable.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from "../src/microstates"; 4 | import { valueOf } from "../src/meta"; 5 | import ArrayType from "../src/types/array"; 6 | import SymbolObservable from 'symbol-observable'; 7 | import { from } from 'rxjs'; 8 | 9 | describe('rxjs interop', function() { 10 | let ms, last; 11 | let observerCalls; 12 | beforeEach(() => { 13 | observerCalls = 0; 14 | ms = create(Number, 42); 15 | from(ms).subscribe(next => { 16 | observerCalls++; 17 | return last = next; 18 | }); 19 | 20 | last.increment(); 21 | last.increment(); 22 | last.increment(); 23 | }); 24 | 25 | it('sent 4 states to obsever', function() { 26 | expect(observerCalls).toBe(4); 27 | }); 28 | 29 | it('incremented 3 times', function() { 30 | expect(valueOf(last)).toBe(45); 31 | }); 32 | }); 33 | 34 | describe('interop', function() { 35 | let ms, observable; 36 | beforeEach(() => { 37 | ms = create(Number, 10); 38 | observable = ms[SymbolObservable](); 39 | }); 40 | 41 | it('observable has subscribe', () => { 42 | expect(observable.subscribe).toBeInstanceOf(Function); 43 | }); 44 | 45 | it('observable has reference to self', () => { 46 | expect(observable[SymbolObservable]()).toBe(observable); 47 | }); 48 | }); 49 | 50 | describe("initial value", function() { 51 | let observable, last; 52 | beforeEach(function() { 53 | let ms = create(Number, 10); 54 | observable = ms[SymbolObservable](); 55 | observable.subscribe(v => (last = v)); 56 | }); 57 | 58 | it("comes from microstate", function() { 59 | expect(last.state).toBe(10); 60 | }); 61 | }); 62 | 63 | describe("single transition", function() { 64 | let observable, last; 65 | beforeEach(function() { 66 | let ms = create(Number, 10); 67 | observable = ms[SymbolObservable](); 68 | observable.subscribe(v => (last = v)); 69 | last.increment(); 70 | }); 71 | 72 | it("gets next value after increment", function() { 73 | expect(last.state).toBe(11); 74 | }); 75 | }); 76 | 77 | describe("many transitions", function() { 78 | let observable, last; 79 | beforeEach(function() { 80 | let ms = create(Number, 10); 81 | observable = ms[SymbolObservable](); 82 | observable.subscribe(v => (last = v)); 83 | last 84 | .increment() 85 | .increment() 86 | .increment(); 87 | }); 88 | 89 | it("gets next value after multiple increments", function() { 90 | expect(last.state).toBe(13); 91 | }); 92 | }); 93 | 94 | describe("complex type", function() { 95 | class A { 96 | b = create(class B { 97 | c = create(class C { 98 | values = create(ArrayType); 99 | }); 100 | }); 101 | } 102 | 103 | let observable, last; 104 | beforeEach(function() { 105 | let ms = create(A, { b: { c: { values: ["hello", "world"] } } }); 106 | observable = ms[SymbolObservable](); 107 | observable.subscribe(v => (last = v)); 108 | }); 109 | 110 | it("has deeply nested transitions", function() { 111 | expect(last.b.c.values.push).toBeInstanceOf(Function); 112 | }); 113 | 114 | describe("invoking deeply nested", function() { 115 | beforeEach(function() { 116 | last.b.c.values.push("!!!"); 117 | }); 118 | it("changed the state", function() { 119 | expect(valueOf(last.b.c.values)).toEqual(["hello", "world", "!!!"]); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('initialized microstate', () => { 125 | class Modal { 126 | isOpen = create(class BooleanType {}); 127 | 128 | initialize(value) { 129 | if (!value) { 130 | return create(Modal, { isOpen: true }); 131 | } 132 | return this; 133 | } 134 | } 135 | 136 | it('streams initialized microstate', () => { 137 | let calls = 0; 138 | let state; 139 | let call = function call(next) { 140 | calls++; 141 | state = valueOf(next); 142 | }; 143 | from(create(Modal)).subscribe(call); 144 | expect(calls).toBe(1); 145 | expect(state).toEqual({ 146 | isOpen: true 147 | }); 148 | }); 149 | }); 150 | 151 | describe('array as root', () => { 152 | let list; 153 | beforeEach(() => { 154 | list = create(ArrayType, [{ hello: 'world' }]); 155 | }); 156 | 157 | it('has array with one element', () => { 158 | expect(list).toHaveLength(1); 159 | expect([...list][0].state.hello).toBeDefined(); 160 | }); 161 | 162 | describe('created observable', () => { 163 | let observable, last, isCalledCallback; 164 | beforeEach(() => { 165 | observable = from(list); 166 | observable.subscribe(next => { 167 | isCalledCallback = true; 168 | last = next; 169 | }); 170 | }); 171 | 172 | it('called callback', () => { 173 | expect(isCalledCallback).toBe(true); 174 | }); 175 | 176 | it('has array with one element', () => { 177 | expect(last).toHaveLength(1); 178 | expect([...last][0].state.hello).toBeDefined(); 179 | }); 180 | }); 181 | 182 | }); 183 | -------------------------------------------------------------------------------- /packages/microstates/tests/package.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | import expect from 'expect'; 3 | import * as exports from '../index'; 4 | 5 | describe('package', () => { 6 | it('exports create', () => expect(exports.create).toBeDefined()); 7 | it('exports from', () => expect(exports.from).toBeDefined()); 8 | it('exports map', () => expect(exports.map).toBeDefined()); 9 | it('exports filter', () => expect(exports.filter).toBeDefined()); 10 | it('exports reduce', () => expect(exports.reduce).toBeDefined()); 11 | it('exports store', () => expect(exports.Store).toBeInstanceOf(Function)); 12 | it('exports metaOf', () => expect(exports.metaOf).toBeInstanceOf(Function)); 13 | it('exports valueOf', () => expect(exports.valueOf).toBeInstanceOf(Function)); 14 | it('exports types', () => { 15 | expect(exports.Any).toBeInstanceOf(Function); 16 | expect(exports.Primitive).toBeInstanceOf(Function); 17 | expect(exports.ObjectType).toBeInstanceOf(Function); 18 | expect(exports.ArrayType).toBeInstanceOf(Function); 19 | expect(exports.NumberType).toBeInstanceOf(Function); 20 | expect(exports.StringType).toBeInstanceOf(Function); 21 | expect(exports.BooleanType).toBeInstanceOf(Function); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/microstates/tests/parameterized.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | 4 | import { create } from "../index"; 5 | import { valueOf } from "../src/meta"; 6 | 7 | describe("Parameterized Microstates: ", () => { 8 | describe("sugar", function() { 9 | class Item { 10 | isCompleted = Boolean; 11 | } 12 | 13 | class TodoList { 14 | items = [Item]; 15 | } 16 | 17 | describe("root [Item] to parameterized(Array)", function() { 18 | let m; 19 | beforeEach(function() { 20 | let [ first ] = create([Item], [{ isCompleted: false }]); 21 | m = first.isCompleted.toggle(); 22 | }); 23 | 24 | it("runs transitions on sub items", function() { 25 | let [ first ] = m; 26 | expect(first.isCompleted.state).toBe(true); 27 | }); 28 | }); 29 | 30 | describe('constructing a bare instance with embeded, parameterized arrays', function() { 31 | let m; 32 | beforeEach(function() { 33 | m = create(TodoList); 34 | }); 35 | 36 | it('has no items', function() { 37 | expect(m.items).toHaveLength(0); 38 | }); 39 | }); 40 | 41 | describe('constructing a bare instance with embeded, parameterized objects', function() { 42 | let m; 43 | beforeEach(function() { 44 | m = create(TodoList); 45 | }); 46 | 47 | it('has no items', function() { 48 | expect(m.items).toHaveLength(0); 49 | }); 50 | }); 51 | 52 | describe("composed [Item] to parameterized(Array)", function() { 53 | let m; 54 | beforeEach(function() { 55 | let [ first ] = create(TodoList, { 56 | items: [{ isCompleted: false }] 57 | }).items; 58 | m = first.isCompleted.toggle(); 59 | }); 60 | 61 | it("runs transitions on sub items", function() { 62 | let [ first ] = m.items; 63 | expect(first.isCompleted.state).toBe(true); 64 | }); 65 | }); 66 | 67 | describe("root {[Number]} to parameterized(Object, parameterized(Array, Number))", function() { 68 | let Numbers = [Number]; 69 | let Counters = { Numbers }; 70 | let value = { 71 | oranges: [50, 20], 72 | apples: [1, 2, 45] 73 | }; 74 | let m; 75 | let transitioned; 76 | beforeEach(function() { 77 | m = create(Counters, value); 78 | let [ firstApple] = m.entries.apples; 79 | let [ _, secondOrange ] = firstApple.increment().entries.oranges; // eslint-disable-line no-unused-vars 80 | transitioned = secondOrange.increment(); 81 | }); 82 | 83 | it("uses the same value for state as the value ", function() { 84 | expect(valueOf(m)).toBe(value); 85 | }); 86 | 87 | it("still respects transitions", function() { 88 | let value = { 89 | oranges: [50, 21], 90 | apples: [2, 2, 45] 91 | }; 92 | let [ first ] = transitioned.entries.apples; // eslint-disable-line no-unused-vars 93 | expect(valueOf(transitioned)).toEqual(value); 94 | }); 95 | }); 96 | 97 | describe("composed {[Number]} to parameterized(Object, parameterized(Array, Number))", function() { 98 | let Numbers = [Number]; 99 | class Store { 100 | inventory = { Numbers }; 101 | } 102 | let m, value; 103 | beforeEach(function() { 104 | value = { 105 | inventory: { 106 | oranges: [50, 20], 107 | apples: [1, 2, 45] 108 | } 109 | }; 110 | m = create(Store, value); 111 | }); 112 | 113 | it("still respects transitions", function() { 114 | let [ firstApple] = m.inventory.entries.apples; 115 | let [ _, secondOrange ] = firstApple.increment().inventory.entries.oranges; // eslint-disable-line no-unused-vars 116 | let next = secondOrange.increment(); 117 | expect(valueOf(next)).toEqual({ 118 | inventory: { 119 | oranges: [50, 21], 120 | apples: [2, 2, 45] 121 | } 122 | }); 123 | }); 124 | }); 125 | }); 126 | 127 | describe("with complex parameters", function() { 128 | let TodoList, Item, m; 129 | beforeEach(function() { 130 | Item = class Item { 131 | isCompleted = Boolean; 132 | description = String; 133 | }; 134 | TodoList = class TodoList { 135 | items = [Item]; 136 | }; 137 | let [ first ] = create(TodoList, { 138 | items: [ 139 | { isCompleted: false, description: "Get Milk" }, 140 | { isCompleted: false, description: "Feed Dog" } 141 | ] 142 | }).items; 143 | m = first.isCompleted.toggle(); 144 | }); 145 | it("runs transitions on sub items", function() { 146 | expect(m).toBeInstanceOf(TodoList); 147 | expect(m.items).toHaveLength(2); 148 | let value = valueOf(m); 149 | expect(value.items[0].isCompleted).toBe(true); 150 | 151 | let [ first, second ] = m.items; 152 | expect(first).toBeInstanceOf(Item); 153 | expect(second.isCompleted.state).toBe(false); 154 | }); 155 | }); 156 | 157 | describe("with simple parameters", function() { 158 | let m, value; 159 | beforeEach(function() { 160 | let PriceList = create({}).constructor.Type.of([Number]); 161 | value = { 162 | oranges: [50, 20], 163 | apples: [1, 2, 45] 164 | }; 165 | m = create(PriceList, value); 166 | }); 167 | 168 | it("still respects transitions", function() { 169 | let [ firstApple] = m.entries.apples; 170 | let [ _, secondOrange ] = firstApple.increment().entries.oranges; // eslint-disable-line no-unused-vars 171 | let next = secondOrange.increment(); 172 | expect(valueOf(next)).toEqual({ 173 | oranges: [50, 21], 174 | apples: [2, 2, 45] 175 | }); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /packages/microstates/tests/pathmap.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, beforeEach, it */ 2 | 3 | import expect from 'expect'; 4 | 5 | import Pathmap from '../src/pathmap'; 6 | import { create, valueOf, BooleanType, ArrayType, NumberType } from '../index'; 7 | 8 | describe('pathmap', ()=> { 9 | 10 | let pathmap; 11 | let LightSwitch; 12 | let ref; 13 | let id; 14 | let hall; 15 | let closet; 16 | 17 | let current; 18 | beforeEach(()=> { 19 | ref = Ref({}); 20 | LightSwitch = class LightSwitch { 21 | hall = Boolean; 22 | closet = Boolean; 23 | }; 24 | pathmap = Pathmap(LightSwitch, ref); 25 | id = pathmap.get(); 26 | hall = id.hall; 27 | closet = id.closet; 28 | 29 | current = pathmap.get; 30 | }); 31 | 32 | it('exists', () => { 33 | expect(pathmap).toBeDefined(); 34 | }); 35 | it('has an id delegate which is represents the microstate at the base path', ()=> { 36 | expect(id).toBeInstanceOf(LightSwitch); 37 | expect(valueOf(id)).toBe(ref.get()); 38 | }); 39 | 40 | it('has children corresponding to the substates', ()=> { 41 | expect(id.hall).toBeInstanceOf(BooleanType); 42 | expect(id.closet).toBeInstanceOf(BooleanType); 43 | }); 44 | 45 | describe('transitioning a substate', ()=> { 46 | beforeEach(()=> { 47 | id.hall.set(true); 48 | }); 49 | it('updates the reference', ()=> { 50 | expect(ref.get()).toEqual({ 51 | hall: true 52 | }); 53 | }); 54 | it('changes the object representing the reference to the toggled switch', ()=> { 55 | expect(pathmap.get(id).hall).not.toBe(hall); 56 | }); 57 | it('changes the root reference if you fetch it again from the pathmap', ()=> { 58 | expect(pathmap.get()).not.toBe(id); 59 | }); 60 | it('leaves the object representing the un-touched switch to be the same', ()=> { 61 | expect(id.closet).toBe(closet); 62 | }); 63 | 64 | it('can fetch the current value based off of the old one.', ()=> { 65 | expect(current(hall)).toBe(current(id).hall); 66 | expect(current(id)).toBe(pathmap.get()); 67 | }); 68 | 69 | it('keeps all the methods stable at each location.', ()=> { 70 | expect(hall.set).toBe(id.hall.set); 71 | }); 72 | }); 73 | 74 | describe('working with arrays', ()=> { 75 | beforeEach(()=> { 76 | pathmap = Pathmap(ArrayType.of(Number), Ref([1, 2, 3])); 77 | id = pathmap.get(); 78 | }); 79 | it('creates a proxy object for all of its children', ()=> { 80 | let [ one, two, three ] = id; 81 | expect(one).toBeInstanceOf(NumberType); 82 | expect(one.constructor.name).toBe('Ref'); 83 | expect(two).toBeInstanceOf(NumberType); 84 | expect(two.constructor.name).toBe('Ref'); 85 | expect(three).toBeInstanceOf(NumberType); 86 | expect(three.constructor.name).toBe('Ref'); 87 | }); 88 | 89 | describe('transitioning one of the contents', ()=> { 90 | let first, second, third; 91 | beforeEach(()=> { 92 | let [one, two, three] = id; 93 | first = one; 94 | second = two; 95 | third = three; 96 | two.increment(); 97 | }); 98 | it('changes the root id', ()=> { 99 | expect(current(id)).not.toBe(id); 100 | }); 101 | it('changes the array member that changed.', ()=> { 102 | expect(current(second)).not.toBe(second); 103 | }); 104 | it('leaves the remaining children that did not change alone', ()=> { 105 | expect(current(first)).toBe(first); 106 | expect(current(third)).toBe(third); 107 | }); 108 | }); 109 | 110 | }); 111 | describe('default values of references in the pathmap', function() { 112 | beforeEach(function() { 113 | pathmap = Pathmap(class { number = create(Number, 42) }, Ref(undefined)); 114 | id = pathmap.get(); 115 | }); 116 | it('maintains the default value of the number', function() { 117 | expect(id.number.state).toBe(42); 118 | }); 119 | }); 120 | 121 | }); 122 | 123 | function Ref(value) { 124 | let ref = { 125 | get() { return value; }, 126 | set(newValue) { 127 | value = newValue; 128 | return ref; 129 | } 130 | }; 131 | return ref; 132 | } 133 | -------------------------------------------------------------------------------- /packages/microstates/tests/query.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../src/microstates'; 4 | import { valueOf } from '../src/meta'; 5 | import { map, filter, reduce, find } from '../src/query'; 6 | 7 | import { TodoMVC } from './todomvc'; 8 | 9 | describe('A Microstate with queries', function() { 10 | let start, todomvc; 11 | 12 | beforeEach(function() { 13 | start = create(TodoMVC) 14 | .todos.push({title: "Take out The Milk"}) 15 | .todos.push({title: "Convince People Microstates is awesome"}) 16 | .todos.push({title: "Take out the Trash"}) 17 | .todos.push({title: "profit $$"}); 18 | let [ first ] = start.todos; 19 | let [,, third ] = first.toggle().todos; 20 | todomvc = third.toggle(); 21 | }); 22 | 23 | it('can partition an array microstate using filter', function() { 24 | let [ first, second, third, fourth ] = todomvc.todos; 25 | let [ firstCompleted, secondCompleted] = todomvc.completed; 26 | let [ firstActive, secondActive ] = todomvc.active; 27 | 28 | expect(todomvc.completed).toHaveLength(2); 29 | expect(valueOf(firstCompleted)).toEqual(valueOf(first)); 30 | expect(valueOf(secondCompleted)).toEqual(valueOf(third)); 31 | 32 | expect(todomvc.active).toHaveLength(2); 33 | expect(valueOf(firstActive)).toEqual(valueOf(second)); 34 | expect(valueOf(secondActive)).toEqual(valueOf(fourth)); 35 | }); 36 | 37 | describe('invoking a transition from one of the object returned by a query.', function() { 38 | let next; 39 | beforeEach(function() { 40 | let [ first ] = todomvc.active; 41 | next = first.toggle(); 42 | }); 43 | 44 | it('has the desired effect on the original item', function() { 45 | let [, second] = next.todos; 46 | expect(second.completed.state).toEqual(true); 47 | expect(next.active).toHaveLength(1); 48 | expect(next.completed).toHaveLength(3); 49 | }); 50 | }); 51 | 52 | describe('finding an element within an iterable Microstate', function() { 53 | let todos, first, third; 54 | beforeEach(function(){ 55 | todos = start.todos; 56 | [first, , third] = start.todos; 57 | }); 58 | 59 | it('can locate the desired item', function() { 60 | expect(valueOf(find(todos, x => x.title.state == "Take out the Trash"))).toBe(valueOf(third)); 61 | }); 62 | 63 | it('returns only the first result', function() { 64 | expect(valueOf(find(todos, x => x.title.state.includes("out")))).toBe(valueOf(first)); 65 | expect(valueOf(find(todos, x => x.title.state.includes("out")))).not.toBe(valueOf(third)); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('Query Array', () => { 71 | let array = [true, false, true]; 72 | 73 | it('can reduce a regular array', () => { 74 | expect(reduce(array, (acc, bool) => bool ? ++acc : acc, 0)).toBe(2); 75 | }); 76 | 77 | it('can map a regular array', () => { 78 | expect([...map(array, bool => !bool)]).toEqual([false, true, false]); 79 | }); 80 | 81 | it('can filter a regular array', () => { 82 | expect([...filter(array, Boolean)]).toEqual([true, true]); 83 | }); 84 | 85 | it('can find specific items inside a normal array', ()=>{ 86 | expect(find(array, bool => bool == false)).toBe(false); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /packages/microstates/tests/recursive.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | 4 | import { Any } from '../src/types'; 5 | import { create } from '../src/microstates'; 6 | import { valueOf } from '../src/meta'; 7 | 8 | describe('recursive microstates', () => { 9 | 10 | class Cons { 11 | car = Any; 12 | cdr = Cons; 13 | } 14 | 15 | let cons; 16 | beforeEach(function() { 17 | cons = create(Cons, { car: 5}); 18 | }); 19 | 20 | it('doesnt blow up', function() { 21 | expect(cons).toBeDefined(); 22 | expect(valueOf(cons.car)).toEqual(5); 23 | }); 24 | 25 | it('can handle recursive things', function() { 26 | expect(cons.cdr.cdr.cdr).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/microstates/tests/relationship.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | 3 | import expect from 'expect'; 4 | 5 | import { Any } from '../src/types'; 6 | import { relationship, Edge } from '../src/relationship'; 7 | 8 | describe('relationships', () => { 9 | let rel; 10 | let e = (value) => new Edge(value, []); 11 | 12 | describe('with absolutely nothing specified', () => { 13 | beforeEach(() => { 14 | rel = relationship(10); 15 | }); 16 | 17 | it('reifies to using Any for the type and the passed value, }', () => { 18 | expect(rel.traverse(e(5))).toEqual({ Type: Any, value: 5 }); 19 | }); 20 | 21 | it('uses the supplied value if non is given', () => { 22 | expect(rel.traverse(e())).toEqual({ Type: Any, value: 10 }); 23 | }); 24 | }); 25 | 26 | describe('depending on the parent microstate that they are a part of', () => { 27 | beforeEach(() => { 28 | rel = relationship((value, parent) => { 29 | if (typeof parent === 'number') { 30 | return { Type: Number, value: Number(value) }; 31 | } else if (typeof parent === 'string') { 32 | return { Type: String, value: String(value) }; 33 | } else { 34 | return { Type: Object, value: Object(value) }; 35 | } 36 | }); 37 | }); 38 | it('generates numbers when the parent is a number', () => { 39 | expect(rel.traverse(5, 'parent')).toEqual({ Type: String, value: '5'}); 40 | }); 41 | 42 | it('generates strings when the parent is a string', () => { 43 | expect(rel.traverse(5, 0)).toEqual({ Type: Number, value: 5}); 44 | }); 45 | }); 46 | 47 | describe('that are mapped', () => { 48 | beforeEach(() => { 49 | rel = relationship() 50 | .map(({ value }) => ({Type: Number, value: value * 2 })); 51 | }); 52 | 53 | it('executes the mapping as part of the traversal', () => { 54 | expect(rel.traverse(e(3))).toEqual({Type: Number, value: 6}); 55 | }); 56 | }); 57 | 58 | describe('Edges', () => { 59 | let edge; 60 | beforeEach(() => { 61 | edge = new Edge({one: {two: 2}}, ['one', 'two']); 62 | }); 63 | it('knows its value', () => { 64 | expect(edge.value).toEqual(2); 65 | }); 66 | it('knows its parent value', () => { 67 | expect(edge.parentValue).toEqual({one: {two: 2}}); 68 | }); 69 | it('has a name provided it is not anonymous', () => { 70 | expect(edge.name).toEqual('two'); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/microstates/tests/setup: -------------------------------------------------------------------------------- 1 | // -*- mode: js2 -*- 2 | 3 | require('object.getownpropertydescriptors/shim')() 4 | 5 | require("@babel/register")({ 6 | // This will override `node_modules` ignoring - you can alternatively pass 7 | // an array of strings to be explicitly matched or a regex / glob 8 | ignore: ['node_modules/*'], 9 | plugins: [ 10 | '@babel/plugin-proposal-class-properties' 11 | ] 12 | }); 13 | -------------------------------------------------------------------------------- /packages/microstates/tests/todomvc.js: -------------------------------------------------------------------------------- 1 | import { create, filter } from '../index'; 2 | 3 | export class Todo { 4 | title = String; 5 | completed = create(Boolean, false); 6 | 7 | toggle() { 8 | return this.completed.set(!this.completed.state); 9 | } 10 | } 11 | 12 | export class TodoMVC { 13 | todos = [Todo]; 14 | 15 | get completed() { 16 | return filter(this.todos, todo => todo.completed.state); 17 | } 18 | 19 | get active() { 20 | return filter(this.todos, todo => !todo.completed.state); 21 | } 22 | 23 | completeAll() { 24 | return this.todos.map(todo => todo.completed.set(true)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/microstates/tests/type-shifting.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from "../index"; 4 | import { valueOf } from "../src/meta"; 5 | 6 | describe("type-shifting", () => { 7 | class Shape { 8 | 9 | get state() { 10 | return valueOf(this); 11 | } 12 | 13 | initialize({ a, b, c } = {}) { 14 | if (a && b && c) { 15 | return create(Triangle, { a, b, c }); 16 | } 17 | if (a && b) { 18 | return create(Angle, { a, b }); 19 | } 20 | if (a) { 21 | return create(Line, { a }); 22 | } 23 | return this; 24 | } 25 | } 26 | 27 | class Line extends Shape { 28 | a = Number; 29 | add(b) { 30 | let { a } = this.state; 31 | return create(Angle, { a, b }); 32 | } 33 | } 34 | 35 | class Angle extends Line { 36 | a = Number; 37 | b = Number; 38 | add(c) { 39 | let { a, b } = this.state; 40 | return create(Triangle, { a, b, c }); 41 | } 42 | } 43 | 44 | class Triangle extends Angle { 45 | c = Number; 46 | } 47 | 48 | class Glass { 49 | shape = Shape; 50 | } 51 | 52 | describe("create", function() { 53 | it("can initialize to itself", () => { 54 | let shape = create(Shape, {}); 55 | expect(shape).toBeInstanceOf(Shape); 56 | }); 57 | 58 | it("initializes to first type", () => { 59 | let triangle = create(Shape, { a: 10, b: 20, c: 30 }); 60 | expect(triangle).toBeInstanceOf(Triangle); 61 | expect(triangle.state).toMatchObject({ 62 | a: 10, 63 | b: 20, 64 | c: 30, 65 | }); 66 | }); 67 | 68 | it("initializes to second type", () => { 69 | let angle = create(Shape, { a: 10, b: 20 }); 70 | expect(angle).toBeInstanceOf(Angle); 71 | expect(angle.state).toMatchObject({ 72 | a: 10, 73 | b: 20, 74 | }); 75 | }); 76 | 77 | it(`can be initialized from descendant's create`, function() { 78 | let line = create(Line, { a: 10 }); 79 | expect(line).toBeInstanceOf(Line); 80 | expect(line.state).toMatchObject({ 81 | a: 10, 82 | }); 83 | }); 84 | 85 | it("is used to initialize composed object", function() { 86 | let composed = create(Glass, { shape: { a: 10, b: 20, c: 30 } }); 87 | expect(composed.shape).toBeInstanceOf(Triangle); 88 | expect(composed.shape.state).toMatchObject({ 89 | a: 10, 90 | b: 20, 91 | c: 30, 92 | }); 93 | }); 94 | 95 | it("supports being initialized in parameterized arrays", () => { 96 | class Drawing { 97 | shapes = [Shape]; 98 | } 99 | let drawing = create(Drawing, { shapes: [{ a: 10 }, { a: 20, b: 30 }, { a: 100, b: 200, c: 300 }] }); 100 | let [ first, second, third ] = drawing.shapes; 101 | expect(first).toBeInstanceOf(Line); 102 | expect(second).toBeInstanceOf(Angle); 103 | expect(third).toBeInstanceOf(Triangle); 104 | }); 105 | 106 | describe("can type-shift into a parameterized type", () => { 107 | class Container { 108 | static create(content) { 109 | if (Array.isArray(content)) { 110 | return create([String], content); 111 | } else { 112 | return create({ String }, content); 113 | } 114 | } 115 | } 116 | it("can initialize into a parameterized array", () => { 117 | let array = Container.create(["a", "b", "c"]); 118 | expect(valueOf(array)).toMatchObject(["a", "b", "c"]); 119 | let [ first ] = array; 120 | expect(first.concat).toBeInstanceOf(Function); 121 | }); 122 | it("can initialize into a parameterized object", () => { 123 | let object = Container.create({ a: "A", b: "B", c: "C" }); 124 | expect(valueOf(object)).toMatchObject({ a: "A", b: "B", c: "C" }); 125 | expect(object.entries.a.concat).toBeInstanceOf(Function); 126 | }); 127 | }); 128 | }); 129 | 130 | describe("transitions", function() { 131 | let ms = create(Line); 132 | let line, corner, triangle; 133 | beforeEach(() => { 134 | line = ms.a.set(10); 135 | corner = line.add(20); 136 | triangle = corner.add(30); 137 | }); 138 | 139 | it("constructs a line", () => { 140 | expect(line).toBeInstanceOf(Line); 141 | expect(line.state).toMatchObject({ 142 | a: 10, 143 | }); 144 | }); 145 | 146 | it("constructs a Corner", () => { 147 | expect(corner).toBeInstanceOf(Angle); 148 | expect(corner.state).toMatchObject({ 149 | a: 10, 150 | b: 20, 151 | }); 152 | }); 153 | 154 | it("constructs a Triangle", () => { 155 | expect(triangle).toBeInstanceOf(Triangle); 156 | expect(triangle.state).toMatchObject({ 157 | a: 10, 158 | b: 20, 159 | c: 30, 160 | }); 161 | }); 162 | }); 163 | }); 164 | 165 | describe("type-shifting with constant values", () => { 166 | class Async { 167 | content = null; 168 | isLoaded = false; 169 | isLoading = false; 170 | isError = false; 171 | 172 | get state() { 173 | return { 174 | content: this.content.state, 175 | error: this.error ? this.error.state : undefined, 176 | isLoaded: this.isLoaded.state, 177 | isLoading: this.isLoading.state, 178 | isError: this.isError.state 179 | }; 180 | } 181 | 182 | loading() { 183 | return create(AsyncLoading, {}); 184 | } 185 | } 186 | 187 | class AsyncError extends Async { 188 | isError = true; 189 | isLoading = false; 190 | isLoaded = true; 191 | } 192 | 193 | class AsyncLoading extends Async { 194 | isLoading = true; 195 | 196 | loaded(content) { 197 | return create( 198 | class extends AsyncLoaded { 199 | content = content; 200 | }, {} 201 | ); 202 | } 203 | 204 | error(msg) { 205 | return create( 206 | class extends AsyncError { 207 | error = msg; 208 | }, {} 209 | ); 210 | } 211 | } 212 | 213 | class AsyncLoaded extends Async { 214 | isLoaded = true; 215 | isLoading = false; 216 | isError = false; 217 | } 218 | 219 | describe("successful loading siquence", () => { 220 | let async = create(Async, {}); 221 | 222 | it("can transition to loading", () => { 223 | let loading = async.loading(); 224 | expect(loading.content.state).toEqual(null); 225 | expect(loading.isLoaded.state).toEqual(false); 226 | expect(loading.isLoading.state).toEqual(true); 227 | expect(loading.isError.state).toEqual(false); 228 | }); 229 | 230 | it("can transition from loading to loaded", () => { 231 | expect(async.loading().loaded("GREAT SUCCESS").state).toMatchObject({ 232 | content: "GREAT SUCCESS", 233 | isLoaded: true, 234 | isLoading: false, 235 | isError: false, 236 | }); 237 | }); 238 | }); 239 | 240 | describe("error loading sequence", () => { 241 | let async = create(Async); 242 | 243 | it("can transition from loading to error", () => { 244 | expect(async.loading().error(":(").state).toMatchObject({ 245 | content: null, 246 | isLoaded: true, 247 | isError: true, 248 | isLoading: false, 249 | error: ":(", 250 | }); 251 | }); 252 | }); 253 | }); 254 | 255 | describe("type-shifting into a deeply composed microstate", () => { 256 | class Node { 257 | name = String; 258 | node = Node; 259 | } 260 | 261 | let root; 262 | beforeEach(() => { 263 | root = create(Node); 264 | }); 265 | 266 | describe("shifting the root node", () => { 267 | let shiftedRoot; 268 | beforeEach(() => { 269 | shiftedRoot = root.set(create(Node, { name: "n1", node: { name: "n2", node: { name: "n3" } } })); 270 | }); 271 | 272 | it("preserves type shifting value", () => { 273 | expect(valueOf(shiftedRoot)).toMatchObject({ 274 | name: "n1", 275 | node: { name: "n2", node: { name: "n3" } }, 276 | }); 277 | }); 278 | }); 279 | 280 | describe("shifting deeply composted state with new value", () => { 281 | let shiftedDeeply; 282 | beforeEach(() => { 283 | shiftedDeeply = root.node.node.node.set({ name: "soooo deep", node: { name: "one more" } }); 284 | }); 285 | 286 | it("preserves type shifting value", () => { 287 | expect(valueOf(shiftedDeeply)).toMatchObject({ 288 | node: { 289 | node: { 290 | node: { 291 | name: "soooo deep", 292 | node: { name: "one more" } 293 | }, 294 | }, 295 | }, 296 | }); 297 | }); 298 | }); 299 | }); 300 | 301 | describe("type-shifting from create to parameterized array", () => { 302 | class Person { 303 | name = String; 304 | } 305 | 306 | class Group { 307 | members = [Person]; 308 | 309 | initialize({ members } = {}) { 310 | if (!members) { 311 | return this.members.set([{ name: "Taras" }, { name: "Charles" }, { name: "Siva" }]); 312 | } 313 | return this; 314 | } 315 | } 316 | 317 | let group; 318 | 319 | beforeEach(() => { 320 | group = create(Group, {}); 321 | }); 322 | 323 | it("initializes to value", () => { 324 | expect(valueOf(group)).toMatchObject({ 325 | members: [{ name: "Taras" }, { name: "Charles" }, { name: "Siva" }], 326 | }); 327 | }); 328 | 329 | it("provides data to parameterized array", () => { 330 | expect(group.members).toHaveLength(3); 331 | 332 | let [ first ] = group.members; 333 | expect(first).toBeInstanceOf(Person); 334 | }); 335 | 336 | describe("transitioning shifted value", () => { 337 | let acclaimed; 338 | 339 | beforeEach(() => { 340 | let [ _, second ] = group.members; // eslint-disable-line no-unused-vars 341 | acclaimed = second.name.set("!!Charles!!"); 342 | }); 343 | 344 | it("has the transitioned state", () => { 345 | expect(valueOf(acclaimed)).toMatchObject({ 346 | members: [{ name: "Taras" }, { name: "!!Charles!!" }, { name: "Siva" }], 347 | }); 348 | }); 349 | }); 350 | }); 351 | 352 | describe("type-shifting from create to parameterized object", () => { 353 | class Parent { 354 | name = String; 355 | } 356 | class Person { 357 | parents = { Parent }; 358 | 359 | initialize({ parents } = {}) { 360 | if (!parents) { 361 | return create(Person, { 362 | parents: { 363 | father: { 364 | name: "John Doe", 365 | }, 366 | mother: { 367 | name: "Jane Doe", 368 | }, 369 | }, 370 | }); 371 | } 372 | return this; 373 | } 374 | } 375 | 376 | let person; 377 | beforeEach(() => { 378 | person = create(Person); 379 | }); 380 | 381 | it("has name with initial values", () => { 382 | expect(person.parents.entries.father).toBeInstanceOf(Parent); 383 | expect(valueOf(person)).toMatchObject({ 384 | parents: { 385 | father: { 386 | name: "John Doe", 387 | }, 388 | mother: { 389 | name: "Jane Doe", 390 | }, 391 | }, 392 | }); 393 | }); 394 | }); 395 | -------------------------------------------------------------------------------- /packages/microstates/tests/types/array.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | 4 | import ArrayType from '../../src/types/array'; 5 | import { create } from '../../src/microstates'; 6 | import { valueOf } from '../../src/meta'; 7 | import { Store } from '../../index'; 8 | 9 | describe("ArrayType", function() { 10 | 11 | describe("created with a scalar value", () => { 12 | let ms; 13 | beforeEach(()=> { 14 | ms = create(ArrayType.of(Number), 1); 15 | }); 16 | 17 | it('wraps the scalar value in an array', ()=> { 18 | expect(valueOf(ms)).toEqual([1]); 19 | }); 20 | }); 21 | 22 | describe("created with an iterator value", () => { 23 | let ms; 24 | beforeEach(()=> { 25 | let iterator = { 26 | 0: 1, 27 | [Symbol.iterator]() { 28 | return [1][Symbol.iterator](); 29 | } 30 | }; 31 | ms = create(ArrayType.of(Number), iterator); 32 | }); 33 | 34 | it('does not wrap iterator in an array', ()=> { 35 | expect([...valueOf(ms)]).toEqual([1]); 36 | }); 37 | 38 | it('result of pushing an item into the array', () => { 39 | expect([...valueOf(ms.push(10))]).toEqual([1, 10]); 40 | }); 41 | }); 42 | 43 | describe("when unparameterized", function() { 44 | let ms; 45 | let array = ["a", "b", "c"]; 46 | 47 | beforeEach(() => { 48 | ms = create(ArrayType, array); 49 | }); 50 | 51 | describe("push", () => { 52 | let pushed; 53 | beforeEach(() => { 54 | pushed = ms.push("d"); 55 | }); 56 | 57 | it("has state", () => { 58 | expect(valueOf(pushed)).toEqual(["a", "b", "c", "d"]); 59 | }); 60 | 61 | describe("again", () => { 62 | let again; 63 | beforeEach(() => { 64 | again = pushed.push("e"); 65 | }); 66 | 67 | it("has state", () => { 68 | expect(valueOf(again)).toEqual(["a", "b", "c", "d", "e"]); 69 | }); 70 | }); 71 | 72 | describe('another microstate', function() { 73 | let again; 74 | beforeEach(function() { 75 | again = pushed.push(create(String, "e")); 76 | }); 77 | 78 | it('uses the value of the microstate, not the microstate itself', function() { 79 | expect(valueOf(again)).toEqual(["a", "b", "c", "d", "e"]); 80 | }); 81 | }); 82 | 83 | }); 84 | 85 | describe("pop", () => { 86 | let popped; 87 | beforeEach(() => { 88 | popped = ms.pop(); 89 | }); 90 | 91 | it("has state", () => { 92 | expect(valueOf(popped)).toEqual(["a", "b"]); 93 | }); 94 | 95 | describe("again", () => { 96 | let again; 97 | beforeEach(() => { 98 | again = popped.pop(); 99 | }); 100 | 101 | it("has state", () => { 102 | expect(valueOf(again)).toEqual(["a"]); 103 | }); 104 | }); 105 | }); 106 | 107 | describe("slice", () => { 108 | let sliced; 109 | beforeEach(() => { 110 | sliced = ms.slice(1); 111 | }); 112 | 113 | it("has a sliced segment of the original list", () => { 114 | expect(valueOf(sliced)).toEqual(["b", "c"]); 115 | }); 116 | 117 | it("returns the same array microstate if all of the values in the underlying array remains the same", () => { 118 | expect(sliced.slice(0)).toBe(sliced); 119 | }); 120 | }); 121 | 122 | describe("sort", () => { 123 | let sorted; 124 | 125 | beforeEach(() => { 126 | let unsorted = create([String], ["c", "b", "a"]); 127 | sorted = unsorted.sort(); 128 | }); 129 | 130 | it("has a value that is the sorted version of the original value", () => { 131 | expect(valueOf(sorted)).toEqual(["a", "b", "c"]); 132 | }); 133 | 134 | it("returns the same array microstate if all of the values in the underlying array remains the same", () => { 135 | expect(sorted.sort()).toBe(sorted); 136 | }); 137 | }); 138 | 139 | describe("filter", () => { 140 | let filtered; 141 | beforeEach(() => { 142 | filtered = ms.filter(v => v.state !== "a"); 143 | }); 144 | 145 | it("state", () => { 146 | expect(valueOf(filtered)).toEqual(["b", "c"]); 147 | }); 148 | 149 | it("returns the same array microstate if all of the values in the underlying array remains the same", () => { 150 | expect(filtered.filter(() => true)).toBe(filtered); 151 | }); 152 | }); 153 | 154 | describe("map", () => { 155 | let mapped; 156 | beforeEach(() => { 157 | mapped = ms.map(v => v.state.toUpperCase()); 158 | }); 159 | 160 | it("state", () => { 161 | expect(valueOf(mapped)).toEqual(["A", "B", "C"]); 162 | }); 163 | 164 | it("returns the same object if the same microstate is returned", () => { 165 | expect(mapped.map(x => x)).toBe(mapped); 166 | }); 167 | 168 | it("returns the same array microstate if all of the values in the underlying array remains the same", () => { 169 | expect(mapped.map(valueOf)).toBe(mapped); 170 | }); 171 | }); 172 | }); 173 | 174 | describe("when parameterized", () => { 175 | class Record { 176 | content = String 177 | } 178 | class Dataset { 179 | records = create(ArrayType.of(Record), []); 180 | } 181 | 182 | describe('empty data set', () => { 183 | let dataset; 184 | beforeEach(() => { 185 | dataset = create(Dataset, { records: [] }); 186 | }); 187 | 188 | describe("pushing a record", () => { 189 | let pushed; 190 | beforeEach(() => { 191 | pushed = dataset.records.push({ content: "Hi!" }); 192 | }); 193 | 194 | it("has the new record", () => { 195 | let [ first ] = pushed.records; 196 | expect(first).toBeInstanceOf(Record); 197 | }); 198 | 199 | it("has given value", () => { 200 | let [ first ] = pushed.records; 201 | expect(first.content.state).toBe("Hi!"); 202 | }); 203 | 204 | describe("changing record", () => { 205 | let changed; 206 | beforeEach(() => { 207 | let [ first ] = pushed.records; 208 | changed = first.content.set("Hello!"); 209 | }); 210 | 211 | it("has changed value", () => { 212 | let [ first ] = changed.records; 213 | expect(first.content.state).toBe("Hello!"); 214 | }); 215 | 216 | describe("popping a record", () => { 217 | let popped; 218 | beforeEach(() => { 219 | popped = changed.records.pop(); 220 | }); 221 | 222 | it("does not have any records", () => { 223 | expect(popped.records).toHaveLength(0); 224 | expect(valueOf(popped.records)).toEqual([]); 225 | }); 226 | }); 227 | }); 228 | }); 229 | }); 230 | 231 | describe('preloaded data set', () => { 232 | let dataset; 233 | beforeEach(() => { 234 | dataset = create(Dataset, { records: [ 235 | {content: 'Herro'}, 236 | {content: 'Sweet'}, 237 | {content: "Woooo"} 238 | ]}); 239 | }); 240 | 241 | describe("push", () => { 242 | let pushed, fourth; 243 | beforeEach(() => { 244 | pushed = dataset.records.push({ content: "Hi!" }); 245 | let first, second, third; // eslint-disable-line no-unused-vars 246 | [first, second, third, fourth] = pushed.records; 247 | }); 248 | 249 | it("has the new record", () => { 250 | expect(fourth).toBeInstanceOf(Record); 251 | }); 252 | 253 | it("has given value", () => { 254 | expect(fourth.content.state).toBe("Hi!"); 255 | }); 256 | 257 | describe("changing record", () => { 258 | let changed; 259 | beforeEach(() => { 260 | changed = fourth.content.set("Hello!"); 261 | }); 262 | 263 | it("has changed value", () => { 264 | let [first, second, third, fourth] = changed.records; // eslint-disable-line no-unused-vars 265 | expect(fourth.content.state).toBe("Hello!"); 266 | }); 267 | }); 268 | }); 269 | 270 | describe('pop', () => { 271 | let popped; 272 | beforeEach(() => { 273 | popped = dataset.records.pop(); 274 | }); 275 | 276 | it('removed last element from the array and changed length', () => { 277 | expect(popped.records).toHaveLength(2); 278 | }); 279 | 280 | describe('changing record', () => { 281 | let changed; 282 | beforeEach(() => { 283 | let [ _, second ] = popped.records; // eslint-disable-line no-unused-vars 284 | changed = second.content.concat('!!!'); 285 | }); 286 | 287 | it('changed the content', () => { 288 | let [ _, second ] = changed.records; // eslint-disable-line no-unused-vars 289 | expect(second.content.state).toBe('Sweet!!!'); 290 | }); 291 | }); 292 | }); 293 | 294 | describe('shift', () => { 295 | let shifted; 296 | beforeEach(() => { 297 | shifted = dataset.records.shift(); 298 | }); 299 | 300 | it('removed first element from the array', () => { 301 | let [ first ] = shifted.records; 302 | expect(first.content.state).toBe('Sweet'); 303 | }); 304 | 305 | it('changed length', () => { 306 | expect(shifted.records).toHaveLength(2); 307 | }); 308 | 309 | describe('changing record', () => { 310 | let changed; 311 | beforeEach(() => { 312 | let [ _, second ] = shifted.records; // eslint-disable-line no-unused-vars 313 | changed = second.content.concat('!!!'); 314 | }); 315 | 316 | it('changed the content', () => { 317 | let [ _, second ] = changed.records; // eslint-disable-line no-unused-vars 318 | expect(second.content.state).toBe('Woooo!!!'); 319 | }); 320 | }); 321 | }); 322 | 323 | describe('unshift', () => { 324 | let unshifted; 325 | beforeEach(() => { 326 | unshifted = dataset.records.unshift({ content: "Hi!" }); 327 | }); 328 | 329 | it('pushed record to the beginning of the array', () => { 330 | let [ first ] = unshifted.records; 331 | expect(first.content.state).toBe('Hi!'); 332 | }); 333 | 334 | it('moved first record to second position', () => { 335 | let [ _, second ] = unshifted.records; // eslint-disable-line no-unused-vars 336 | expect(second.content.state).toBe('Herro'); 337 | }); 338 | 339 | describe('change new record', () => { 340 | let changed; 341 | beforeEach(() => { 342 | let [ first ] = unshifted.records; 343 | changed = first.content.concat('!!!'); 344 | }); 345 | 346 | it('changed new record', () => { 347 | let [ first ] = changed.records; 348 | expect(first.content.state).toBe('Hi!!!!'); 349 | }); 350 | }); 351 | 352 | describe('change existing record', () => { 353 | let changed; 354 | beforeEach(() => { 355 | let [_, second ] = unshifted.records; // eslint-disable-line no-unused-vars 356 | changed = second.content.concat('!!!'); 357 | }); 358 | 359 | it('changed new record', () => { 360 | let [_, second ] = changed.records; // eslint-disable-line no-unused-vars 361 | expect(second.content.state).toBe('Herro!!!'); 362 | }); 363 | }); 364 | 365 | describe('a microstate', () => { 366 | beforeEach(()=> { 367 | unshifted = dataset.records.unshift(create(null, { content: "Hello World!" })); 368 | }); 369 | 370 | 371 | it('uses the value of the microstate and not the microstate itself.', function() { 372 | let [ first ] = valueOf(unshifted.records); 373 | expect(first).toEqual({ content: 'Hello World!'}); 374 | }); 375 | }); 376 | 377 | }); 378 | 379 | describe('filter', () => { 380 | let filtered; 381 | beforeEach(() => { 382 | filtered = dataset.records.filter(record => record.content.state[0] === 'S'); 383 | }); 384 | 385 | it('filtered out items', () => { 386 | expect(filtered.records).toHaveLength(1); 387 | }); 388 | 389 | describe('changing remaining item', () => { 390 | let changed; 391 | beforeEach(() => { 392 | let [ first ] = filtered.records; 393 | changed = first.content.concat('!!!'); 394 | }); 395 | 396 | it('it changed the state', () => { 397 | let [ first ] = changed.records; 398 | expect(first.content.state).toBe('Sweet!!!'); 399 | }); 400 | }); 401 | }); 402 | 403 | describe('map', () => { 404 | describe('with microstate operations', () => { 405 | let mapped; 406 | beforeEach(() => { 407 | mapped = dataset.records.map(record => record.content.concat('!!!')); 408 | }); 409 | 410 | it('applied change to every element', () => { 411 | let [ first, second, third ] = mapped.records; 412 | expect(first.content.state).toBe('Herro!!!'); 413 | expect(second.content.state).toBe('Sweet!!!'); 414 | expect(third.content.state).toBe('Woooo!!!'); 415 | }); 416 | 417 | describe('changing record', () => { 418 | let changed; 419 | beforeEach(() => { 420 | let [_, second ] = mapped.records; // eslint-disable-line no-unused-vars 421 | changed = second.content.set('SWEET!!!'); 422 | }); 423 | 424 | it('changed the record content', () => { 425 | let [_, second ] = changed.records; // eslint-disable-line no-unused-vars 426 | expect(second.content.state).toBe('SWEET!!!'); 427 | }); 428 | }); 429 | }); 430 | }); 431 | 432 | describe('clear', () => { 433 | let cleared; 434 | beforeEach(() => { 435 | cleared = dataset.records.clear(); 436 | }); 437 | 438 | it('makes array empty', () => { 439 | expect(cleared.records).toHaveLength(0); 440 | }); 441 | 442 | it('has empty value', () => { 443 | expect(valueOf(cleared)).toEqual({ records: [] }); 444 | }); 445 | }); 446 | }); 447 | }); 448 | 449 | describe('iteration', () => { 450 | let array, substates; 451 | beforeEach(() => { 452 | array = create([], [{a: "a"}, {b: "b"}, {c: "c"}]); 453 | [...substates] = array; 454 | }); 455 | 456 | it('creates a substate for each element in the array', function() { 457 | expect(substates).toHaveLength(3); 458 | }); 459 | 460 | it('has undefined as the value of a done iteration', function() { 461 | let iterator = array[Symbol.iterator](); 462 | iterator.next(); 463 | iterator.next(); 464 | iterator.next(); 465 | let last = iterator.next(); 466 | expect(last.done).toBe(true); 467 | expect(last.value).toBe(undefined); 468 | }); 469 | 470 | describe('transitinging one of the substates', function() { 471 | let transitioned; 472 | beforeEach(() => { 473 | transitioned = substates[1].set({b: "bee"}); 474 | }); 475 | 476 | it('returns an array of the same number of elements', function() { 477 | expect(transitioned).toBeInstanceOf(ArrayType); 478 | expect(valueOf(transitioned)).toEqual([{a: "a"}, {b: "bee"}, {c: "c"}]); 479 | }); 480 | }); 481 | }); 482 | 483 | describe('remove', () => { 484 | let array; 485 | beforeEach(() => { 486 | array = create([], ['a', 'b', 'c']); 487 | }); 488 | 489 | it('allows to remove an element', () => { 490 | let [a, b, c] = array; 491 | let removed = array.remove(b); 492 | let [a2, c2] = removed; 493 | expect(removed.length).toBe(2); 494 | expect(valueOf(a)).toBe(valueOf(a2)); 495 | expect(valueOf(c)).toBe(valueOf(c2)); 496 | }); 497 | 498 | it('returns same array when item is not found', () => { 499 | expect(array.remove(null)).toBe(array); 500 | }); 501 | }); 502 | 503 | describe('within a Store', ()=> { 504 | it('return undefined at the end of an iteration', ()=> { 505 | let array = Store(create([Number], [1,2])); 506 | let iterator = array[Symbol.iterator](); 507 | iterator.next(); 508 | iterator.next(); 509 | expect(iterator.next().value).toBe(undefined); 510 | }); 511 | }); 512 | 513 | }); 514 | -------------------------------------------------------------------------------- /packages/microstates/tests/types/boolean.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../../src/microstates'; 4 | 5 | describe('boolean', () => { 6 | let boolean; 7 | beforeEach(() => { 8 | boolean = create(Boolean); 9 | }); 10 | 11 | it('has state', () => { 12 | expect(boolean.state).toBe(false); 13 | }); 14 | 15 | describe('toggle', () => { 16 | let toggled; 17 | beforeEach(() => { 18 | toggled = boolean.toggle(); 19 | }); 20 | 21 | it('has state', () => { 22 | expect(toggled.state).toBe(true); 23 | }); 24 | }); 25 | 26 | it('converts falsy values into false', function() { 27 | expect(boolean.set(null).state).toBe(false); 28 | expect(boolean.set('').state).toBe(false); 29 | expect(boolean.set(undefined).state).toBe(false); 30 | expect(boolean.set(0).state).toBe(false); 31 | }); 32 | 33 | it('converts truthy values into true', function() { 34 | expect(boolean.set('foo').state).toBe(true); 35 | expect(boolean.set(1).state).toBe(true); 36 | expect(boolean.set({}).state).toBe(true); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/microstates/tests/types/number.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../../index'; 4 | 5 | describe("number", () => { 6 | let zero, ten, str; 7 | beforeEach(() => { 8 | zero = create(Number); 9 | ten = create(Number, 10); 10 | str = create(Number, 'hello'); 11 | }); 12 | 13 | it("has transitions", () => { 14 | expect(typeof zero.set).toBe("function"); 15 | expect(typeof zero.increment).toBe("function"); 16 | expect(typeof zero.decrement).toBe("function"); 17 | }); 18 | 19 | describe("without value", () => { 20 | it("has state", () => { 21 | expect(zero.state).toBe(0); 22 | }); 23 | 24 | it("increment", () => { 25 | expect(zero.increment().state).toBe(1); 26 | }); 27 | 28 | it("decrement", () => { 29 | expect(zero.decrement().state).toBe(-1); 30 | }); 31 | 32 | it("increment passed a value", ()=> { 33 | expect(zero.increment(10).state).toBe(10); 34 | }); 35 | }); 36 | 37 | describe("with value", () => { 38 | it("has state", () => { 39 | expect(ten.state).toBe(10); 40 | }); 41 | 42 | it("increment", () => { 43 | expect(ten.increment().state).toBe(11); 44 | }); 45 | 46 | it("decrement", () => { 47 | expect(ten.decrement().state).toBe(9); 48 | }); 49 | 50 | it("decrement passed a value", () => { 51 | expect(ten.decrement(10).state).toBe(0); 52 | }); 53 | }); 54 | 55 | describe("with a NaN value", () => { 56 | it("has state", () => { 57 | expect(str.state).toBe('hello'); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/microstates/tests/types/object.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../../src/microstates'; 4 | import { valueOf } from '../../src/meta'; 5 | import { ObjectType } from '../../src/types'; 6 | import { map } from '../../src/query'; 7 | 8 | describe('created without value', () => { 9 | class Thing {} 10 | let object; 11 | beforeEach(() => { 12 | object = create(ObjectType.of(Thing)); 13 | }); 14 | 15 | it('has empty object as state', () => { 16 | expect(valueOf(object)).toEqual({}); 17 | }); 18 | 19 | describe('assign once', () => { 20 | let assigned; 21 | beforeEach(() => { 22 | assigned = object.assign({ foo: 'bar' }); 23 | }); 24 | 25 | it('received the assigned value', () => { 26 | expect(valueOf(assigned)).toEqual({ foo: 'bar' }); 27 | }); 28 | 29 | it('wraps the assigned values the parameterized type', function() { 30 | expect(assigned.entries.foo).toBeInstanceOf(Thing); 31 | expect(valueOf(assigned.entries.foo)).toEqual('bar'); 32 | }); 33 | 34 | describe('assign twice', () => { 35 | let assignedAgain; 36 | beforeEach(() => { 37 | assignedAgain = assigned.assign({ bar: 'baz' }); 38 | }); 39 | 40 | it('received the assigned value', () => { 41 | expect(valueOf(assignedAgain)).toEqual({ foo: 'bar', bar: 'baz' }); 42 | }); 43 | 44 | it('maintains stability of the state', function() { 45 | expect(valueOf(assignedAgain).foo).toBe(valueOf(assigned).foo); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('created with value', () => { 52 | let object; 53 | beforeEach(() => { 54 | object = create(ObjectType, { foo: 'bar' }); 55 | }); 56 | 57 | it('has empty object as state', () => { 58 | expect(valueOf(object)).toEqual({ foo: 'bar' }); 59 | }); 60 | 61 | describe('assign once', () => { 62 | let assigned; 63 | beforeEach(() => { 64 | assigned = object.assign({ bar: 'baz' }); 65 | }); 66 | 67 | it('received the assigned value', () => { 68 | expect(valueOf(assigned)).toEqual({ foo: 'bar', bar: 'baz' }); 69 | }); 70 | 71 | describe('assign twice', () => { 72 | let assignedAgain; 73 | beforeEach(() => { 74 | assignedAgain = assigned.assign({ zoo: 'zar' }); 75 | }); 76 | 77 | it('received the assigned value', () => { 78 | expect(valueOf(assignedAgain)).toEqual({ foo: 'bar', bar: 'baz', zoo: 'zar' }); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('assign microstate', () => { 84 | describe('primitive type', () => { 85 | let assigned; 86 | beforeEach(() => { 87 | assigned = object.assign({ 88 | name: create(String, 'Taras') 89 | }); 90 | }); 91 | 92 | it('assigned is not a microstate', () => { 93 | expect(assigned.entries.name.state).toBe('Taras'); 94 | }); 95 | 96 | it('microstate value to be part of valueOf', () => { 97 | expect(valueOf(assigned)).toEqual({ foo: 'bar', name: 'Taras' }); 98 | }); 99 | }); 100 | 101 | describe('composed type', () => { 102 | class Person { 103 | name = create(class StringType {}); 104 | } 105 | 106 | let assigned, value; 107 | beforeEach(() => { 108 | value = create(Person, { name: 'Taras' }); 109 | assigned = object.assign({ 110 | taras: value 111 | }); 112 | }); 113 | 114 | it('is stable', () => { 115 | expect(valueOf(assigned).taras).toBe(valueOf(value)); 116 | }); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('put and delete', () => { 122 | let object; 123 | beforeEach(() => { 124 | object = create(ObjectType, {a: 'b'}); 125 | }); 126 | 127 | describe('putting a value or two', function() { 128 | beforeEach(function() { 129 | object = object.put('w', 'x').put('y', 'z'); 130 | }); 131 | 132 | it('includes those values in the state', function() { 133 | expect(valueOf(object)).toEqual({a: 'b', w: 'x', y: 'z'}); 134 | }); 135 | 136 | describe('deleting a value', function() { 137 | beforeEach(() => { 138 | object = object.delete('w'); 139 | }); 140 | 141 | it('removes it from the value', function() { 142 | expect(valueOf(object)).toEqual({a: 'b', y: 'z'}); 143 | }); 144 | }); 145 | }); 146 | 147 | describe('putting microstate', () => { 148 | describe('primitive value', () => { 149 | beforeEach(() => { 150 | object = object.put('name', create(class StringType {}, 'Taras')); 151 | }); 152 | 153 | it('has name string', () => { 154 | expect(object.entries.name.state).toBe('Taras'); 155 | }); 156 | 157 | it('has valueOf', () => { 158 | expect(valueOf(object)).toEqual({ a: 'b', name: 'Taras' }); 159 | }); 160 | }); 161 | 162 | describe('composed type', () => { 163 | class Person { 164 | name = create(class StringType {}); 165 | } 166 | 167 | let value; 168 | beforeEach(() => { 169 | value = create(Person, { name: 'Taras' }); 170 | object = object.put('taras', value); 171 | }); 172 | 173 | it('is stable', () => { 174 | expect(valueOf(object).taras).toBe(valueOf(value)); 175 | }); 176 | }); 177 | }); 178 | }); 179 | 180 | describe("map/filter/reduce", () => { 181 | class Todo { 182 | id = create(String); 183 | completed = create(Boolean, false); 184 | } 185 | 186 | let todosById = create( 187 | { Todo }, 188 | { 189 | a: { 190 | id: "a", 191 | completed: false 192 | }, 193 | b: { 194 | id: "b", 195 | completed: true 196 | }, 197 | c: { 198 | id: "c", 199 | completed: false 200 | } 201 | } 202 | ); 203 | 204 | describe("map", () => { 205 | let mapped; 206 | beforeEach(() => { 207 | mapped = todosById.map(todo => todo.completed.set(true)); 208 | }); 209 | 210 | it("completes each todo", () => { 211 | expect(mapped.entries["a"].completed.state).toEqual(true); 212 | expect(mapped.entries["b"].completed.state).toEqual(true); 213 | expect(mapped.entries["c"].completed.state).toEqual(true); 214 | }); 215 | }); 216 | 217 | describe("filter", () => { 218 | let filtered; 219 | beforeEach(() => { 220 | filtered = todosById.filter(todo => !todo.completed.state); 221 | }); 222 | 223 | it("removes completed todos", () => { 224 | expect(filtered.entries["a"]).toBeDefined(); 225 | expect(filtered.entries["b"]).not.toBeDefined(); 226 | expect(filtered.entries["c"]).toBeDefined(); 227 | }); 228 | }); 229 | 230 | describe('iterable', () => { 231 | let obj, calls; 232 | beforeEach(() => { 233 | obj = create(Object, { a: 'A', b: 'B', c: 'C'}); 234 | calls = []; 235 | }); 236 | 237 | it('supports for of and destructure as object', () => { 238 | for (let { value, key } of obj) { 239 | calls.push({ key, value: value.state }); 240 | } 241 | 242 | expect(calls.length).toBe(3); 243 | expect(valueOf(calls[0])).toEqual({key: 'a', value: 'A'}); 244 | expect(valueOf(calls[1])).toEqual({key: 'b', value: 'B'}); 245 | expect(valueOf(calls[2])).toEqual({key: 'c', value: 'C'}); 246 | }); 247 | 248 | it('supports for of and destructure as array', () => { 249 | for (let [ value, key ] of obj) { 250 | calls.push({ key, value: value.state }); 251 | } 252 | 253 | expect(calls.length).toBe(3); 254 | expect(valueOf(calls[0])).toEqual({key: 'a', value: 'A'}); 255 | expect(valueOf(calls[1])).toEqual({key: 'b', value: 'B'}); 256 | expect(valueOf(calls[2])).toEqual({key: 'c', value: 'C'}); 257 | }); 258 | 259 | it('allows to map object', () => { 260 | for (let o of map(obj, ({ key, value }) => ({ key, value: value.state }))) { 261 | calls.push(o); 262 | } 263 | expect(calls.length).toBe(3); 264 | expect(valueOf(calls[0])).toMatchObject({key: 'a', value: 'A'}); 265 | expect(valueOf(calls[1])).toMatchObject({key: 'b', value: 'B'}); 266 | expect(valueOf(calls[2])).toMatchObject({key: 'c', value: 'C'}); 267 | }); 268 | 269 | it('is undefined at the end of iteration', function() { 270 | let iterator = obj[Symbol.iterator](); 271 | iterator.next(); 272 | iterator.next(); 273 | iterator.next(); 274 | let last = iterator.next(); 275 | expect(last.done).toBe(true); 276 | expect(last.value).toBe(undefined); 277 | }); 278 | }); 279 | }); 280 | 281 | describe('keys and values', function() { 282 | let object; 283 | beforeEach(() => { 284 | object = create({Number}, {one: 1, two: 2, three: 3}); 285 | }); 286 | it('has an enumeration of keys', function() { 287 | let [...keys] = object.keys; 288 | expect(keys).toEqual(['one', 'two', 'three']); 289 | }); 290 | it('has an enumeration of values', function() { 291 | let [ one, two, three ] = object.values; 292 | expect(one.state).toEqual(1); 293 | expect(two.state).toEqual(2); 294 | expect(three.state).toEqual(3); 295 | }); 296 | }); 297 | -------------------------------------------------------------------------------- /packages/microstates/tests/types/string.test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, beforeEach */ 2 | import expect from 'expect'; 3 | import { create } from '../../index'; 4 | 5 | describe('string without value', () => { 6 | let string; 7 | beforeEach(() => { 8 | string = create(String); 9 | }); 10 | 11 | it('has state', () => { 12 | expect(string.state).toBe(''); 13 | }); 14 | 15 | describe('concat', () => { 16 | let concatted; 17 | beforeEach(() => { 18 | concatted = string.concat('hello world'); 19 | }); 20 | 21 | it('has state', () => { 22 | expect(concatted.state).toBe('hello world'); 23 | }); 24 | }); 25 | }); 26 | 27 | describe('string with value', () => { 28 | let string; 29 | beforeEach(() => { 30 | string = create(String, 'hello world'); 31 | }); 32 | 33 | it('has state', () => { 34 | expect(string.state).toBe('hello world'); 35 | }); 36 | 37 | describe('concat', () => { 38 | let concatted; 39 | beforeEach(() => { 40 | concatted = string.concat('!!!'); 41 | }); 42 | 43 | it('has state', () => { 44 | expect(concatted.state).toBe('hello world!!!'); 45 | }); 46 | }); 47 | }); 48 | --------------------------------------------------------------------------------