├── .babelrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.json ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bench ├── compare-tree-performance.js ├── dispatch-performance.js ├── fork-performance.js ├── history-performance.js └── push-performance.js ├── docs ├── README.md ├── api │ ├── action-button.md │ ├── action-form.md │ ├── actions.md │ ├── domains.md │ ├── effects.md │ ├── history.md │ ├── immutability-helpers.md │ ├── index.md │ ├── microcosm.md │ ├── presenter.md │ └── with-send.md ├── guides │ ├── architecture.md │ ├── contributing.md │ ├── index.md │ ├── installation.md │ └── quickstart.md ├── recipes │ ├── ajax.md │ ├── batch-updates.md │ ├── hydrating-state.md │ ├── index.md │ ├── preact.md │ └── react-router.md └── testing │ ├── domains.md │ ├── effects.md │ ├── index.md │ ├── overview.md │ └── presenters.md ├── examples ├── README.md ├── canvas │ ├── Makefile │ ├── app │ │ ├── boot.js │ │ └── prepare-canvas.js │ └── public │ │ └── index.html ├── chatbot │ ├── Makefile │ ├── README.md │ ├── app │ │ ├── actions │ │ │ └── messages.js │ │ ├── boot.js │ │ ├── domains │ │ │ └── messages.js │ │ ├── records │ │ │ └── message.js │ │ ├── repo.js │ │ └── views │ │ │ ├── chat.js │ │ │ └── parts │ │ │ ├── announcer.js │ │ │ ├── conversation.js │ │ │ ├── message.js │ │ │ ├── messenger.js │ │ │ └── prompt.js │ ├── lib │ │ ├── chat.js │ │ └── server.js │ ├── public │ │ ├── index.html │ │ └── style.css │ └── webpack.config.js ├── painter │ ├── Makefile │ ├── README.md │ ├── app │ │ ├── actions │ │ │ └── pixels.js │ │ ├── boot.js │ │ ├── domains │ │ │ └── pixels.js │ │ ├── repo.js │ │ └── views │ │ │ ├── canvas.js │ │ │ ├── row.js │ │ │ └── workspace.js │ └── public │ │ ├── index.html │ │ └── style.css ├── react-router │ ├── Makefile │ ├── README.md │ ├── app │ │ ├── actions │ │ │ ├── items.js │ │ │ ├── lists.js │ │ │ └── routing.js │ │ ├── boot.js │ │ ├── domains │ │ │ ├── domain.js │ │ │ ├── items.js │ │ │ └── lists.js │ │ ├── effects │ │ │ └── routing.js │ │ ├── models │ │ │ └── lists.js │ │ ├── repo.js │ │ └── views │ │ │ ├── application.js │ │ │ ├── errors │ │ │ └── notfound.js │ │ │ └── lists │ │ │ ├── index.js │ │ │ ├── parts │ │ │ ├── item-form.js │ │ │ ├── item-list.js │ │ │ ├── list-form.js │ │ │ └── list-list.js │ │ │ └── show.js │ └── public │ │ ├── index.html │ │ └── style.css ├── simple-svg │ ├── Makefile │ ├── README.md │ ├── app │ │ ├── actions │ │ │ └── animate.js │ │ ├── boot.js │ │ ├── domains │ │ │ └── circle.js │ │ └── views │ │ │ └── logo.js │ └── public │ │ ├── index.html │ │ └── style.css └── webpack.config.js ├── flow ├── command.js ├── domain.js ├── effect.js ├── events.js ├── form-serialize.js ├── presenter.js ├── registry.js ├── snapshot.js └── uid.js ├── jsdoc ├── README.md ├── conf.json ├── scripts │ └── serve ├── templates │ └── microcosm │ │ ├── README.md │ │ ├── publish.js │ │ ├── static │ │ ├── assets │ │ │ ├── chat-debugger.gif │ │ │ ├── microcosm-badge.png │ │ │ ├── microcosm-badge.svg │ │ │ ├── microcosm-white.svg │ │ │ ├── microcosm.png │ │ │ └── microcosm.svg │ │ ├── scripts │ │ │ ├── linenumber.js │ │ │ └── prettify │ │ │ │ ├── Apache-License-2.0.txt │ │ │ │ ├── lang-css.js │ │ │ │ └── prettify.js │ │ └── styles │ │ │ ├── jsdoc-default.css │ │ │ ├── prettify-jsdoc.css │ │ │ ├── prettify-tomorrow.css │ │ │ └── reference-old-styles.css │ │ └── tmpl │ │ ├── augments.tmpl │ │ ├── container.tmpl │ │ ├── details.tmpl │ │ ├── example.tmpl │ │ ├── examples.tmpl │ │ ├── exceptions.tmpl │ │ ├── layout.tmpl │ │ ├── mainpage.tmpl │ │ ├── members.tmpl │ │ ├── method.tmpl │ │ ├── params.tmpl │ │ ├── properties.tmpl │ │ ├── returns.tmpl │ │ ├── source.tmpl │ │ ├── tutorial.tmpl │ │ └── type.tmpl └── tutorials │ ├── quickstart.md │ ├── recipes.md │ ├── recipes │ ├── ajax.md │ └── preact.md │ └── tutorials.json ├── new_site └── .gitignore ├── package.json ├── scripts └── bundle ├── site ├── .gitignore ├── Makefile ├── README.md ├── public │ └── assets │ │ ├── chat-debugger.gif │ │ ├── microcosm-badge.png │ │ ├── microcosm-badge.svg │ │ ├── microcosm-white.svg │ │ ├── microcosm.png │ │ ├── microcosm.svg │ │ └── style.css ├── scripts │ ├── build-page │ ├── publish │ └── serve └── src │ ├── layouts │ ├── default.html │ └── home.html │ └── pages │ └── index.md ├── src ├── action.js ├── addons │ ├── action-button.js │ ├── action-form.js │ ├── frame.js │ ├── indexing.js │ ├── jest-matchers.js │ ├── model.js │ ├── presenter.js │ └── with-send.js ├── compare-tree │ ├── index.js │ ├── node.js │ └── query.js ├── coroutine.js ├── default-update-strategy.js ├── domain-engine.js ├── effect-engine.js ├── emitter.js ├── get-registration.js ├── history.js ├── index.js ├── install-devtools.js ├── key-path.js ├── lifecycle.js ├── meta-domain.js ├── microcosm.js ├── symbols.js ├── tag.js └── utils.js ├── test ├── addons │ ├── action-button.test.js │ ├── action-form.test.js │ ├── indexes.test.js │ ├── model.test.js │ ├── presenter.test.js │ └── with-send.test.js ├── dev.config.json ├── helpers │ ├── mock-send.js │ └── setup.js ├── integration │ ├── efficiency.test.js │ ├── mutation.test.js │ ├── nested-actions.test.js │ ├── reconciliation.test.js │ ├── reexecuting-domain-handlers.test.js │ ├── removal.test.js │ ├── rollbacks.test.js │ └── snapshots-in-forks.test.js ├── prod.config.json └── unit │ ├── action │ ├── callbacks.test.js │ ├── cancelled.test.js │ ├── complete.test.js │ ├── disabled.test.js │ ├── done.test.js │ ├── error.test.js │ ├── link.test.js │ ├── open.test.js │ ├── payload.test.js │ ├── promise-interop.test.js │ ├── prune.test.js │ ├── remove.test.js │ ├── teardown.test.js │ ├── toggle.test.js │ └── update.test.js │ ├── compare-tree │ ├── compare-tree.test.js │ └── fixtures │ │ └── solar-system.js │ ├── domain │ ├── construction.test.js │ ├── deserialize.test.js │ ├── register.test.js │ ├── serialize.test.js │ ├── setup.test.js │ └── teardown.test.js │ ├── effect │ ├── construction.test.js │ ├── register.test.js │ ├── setup.test.js │ └── teardown.test.js │ ├── emitter.test.js │ ├── history │ ├── append.test.js │ ├── archive.test.js │ ├── checkout.test.js │ ├── children.test.js │ ├── isActive.test.js │ ├── iterator.test.js │ ├── map.test.js │ ├── remove.test.js │ ├── setLimit.test.js │ ├── sharedRoot.test.js │ ├── then.test.js │ ├── toArray.test.js │ ├── toJSON.test.js │ ├── toggle.test.js │ ├── updater.test.js │ └── wait.test.js │ ├── key-path │ ├── cast-path.test.js │ └── get-key-paths.test.js │ ├── microcosm │ ├── addDomain.test.js │ ├── checkout.test.js │ ├── constructor.test.js │ ├── devtools.test.js │ ├── dispatch.test.js │ ├── events.test.js │ ├── fork.test.js │ ├── patch.test.js │ ├── prepare.test.js │ ├── push.test.js │ ├── release.test.js │ ├── reset.test.js │ ├── setup.test.js │ └── shutdown.test.js │ ├── middleware │ ├── generator-middleware.test.js │ ├── promise-middleware.test.js │ └── thunk-middleware.test.js │ ├── registration.test.js │ ├── tag.test.js │ └── utils │ ├── get.test.js │ ├── merge.test.js │ ├── set.test.js │ └── update.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "loose": true, 7 | "modules": false 8 | } 9 | ], 10 | "stage-2", 11 | "react", 12 | "flow" 13 | ], 14 | "env": { 15 | "test": { 16 | "presets": [ 17 | "es2015", 18 | "stage-2", 19 | "react" 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | docker: 5 | - image: circleci/node:10 6 | working_directory: ~/repo 7 | 8 | restore-npm-cache: &restore-npm-cache 9 | restore_cache: 10 | keys: 11 | - v1-dependencies-{{ checksum "package.json" }} 12 | - v1-dependencies- 13 | 14 | jobs: 15 | build: 16 | <<: *defaults 17 | steps: 18 | - checkout 19 | - <<: *restore-npm-cache 20 | - run: yarn install 21 | - save_cache: 22 | paths: 23 | - node_modules 24 | key: v1-dependencies-{{ checksum "package.json" }} 25 | audit: 26 | <<: *defaults 27 | steps: 28 | - checkout 29 | - <<: *restore-npm-cache 30 | - run: yarn flow 31 | - run: yarn lint 32 | test-dev: 33 | <<: *defaults 34 | steps: 35 | - checkout 36 | - <<: *restore-npm-cache 37 | - run: yarn test:cov 38 | - run: yarn report-coverage 39 | test-prod: 40 | <<: *defaults 41 | steps: 42 | - checkout 43 | - <<: *restore-npm-cache 44 | - run: yarn test:prod 45 | 46 | workflows: 47 | version: 2 48 | microcosm: 49 | jobs: 50 | - build 51 | - audit: 52 | requires: 53 | - build 54 | - test-dev: 55 | requires: 56 | - build 57 | - test-prod: 58 | requires: 59 | - build 60 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | max_line_length = 80 11 | 12 | # Tab indentation (no size specified) 13 | [Makefile] 14 | indent_style = tab 15 | 16 | # Indentation override for all JS under lib directory 17 | [src/**.js] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Matches the exact files either package.json or .circle.yml 22 | [{package.json,.circle.yml}] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Promise": true, 4 | "jest": true, 5 | "expect": true 6 | }, 7 | "env": { 8 | "browser": true, 9 | "commonjs": true, 10 | "es6": true, 11 | "node": true, 12 | "jest": true 13 | }, 14 | "parser": "babel-eslint", 15 | "parserOptions": { 16 | "ecmaVersion": 7, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "experimentalObjectRestSpread": true, 20 | "jsx": true 21 | } 22 | }, 23 | "settings": { 24 | "flowtype": { 25 | "onlyFilesWithFlowAnnotation": true 26 | }, 27 | "react": { 28 | "version": "15.0" 29 | } 30 | }, 31 | "extends": ["eslint:recommended", "plugin:flowtype/recommended"], 32 | "plugins": [ "react", "flowtype", "flowtype-errors" ], 33 | "rules": { 34 | "semi": [0, "never"], 35 | "sort-vars": 2, 36 | "comma-dangle": [2, "never"], 37 | "no-console": 0, 38 | "quotes": [2, "single", { "avoidEscape": true }], 39 | "no-unused-vars" : [2, { "args": "none" }], 40 | "react/jsx-equals-spacing": ["warn", "never"], 41 | "react/jsx-no-duplicate-props": ["warn", { "ignoreCase": true }], 42 | "react/jsx-no-undef": "error", 43 | "react/jsx-pascal-case": [ 44 | "warn", 45 | { 46 | "allowAllCaps": true, 47 | "ignore": [] 48 | } 49 | ], 50 | "react/jsx-uses-react": "warn", 51 | "react/jsx-uses-vars": "warn", 52 | "react/no-danger-with-children": "warn", 53 | "react/no-deprecated": "warn", 54 | "react/no-direct-mutation-state": "warn", 55 | "react/no-is-mounted": "warn", 56 | "react/react-in-jsx-scope": "error", 57 | "react/require-render-return": "warn", 58 | "react/style-prop-object": "warn" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | .*/build/.* 4 | .*/test/.* 5 | 6 | [include] 7 | src/ 8 | examples/ 9 | 10 | [libs] 11 | flow/ 12 | 13 | [options] 14 | experimental.const_params=true 15 | unsafe.enable_getters_and_setters=true 16 | module.name_mapper='microcosm' -> '/src/index.js' -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Is this a feature, bug, or something else?** 2 | 3 | **What is the current behavior?** 4 | 5 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: https://jsfiddle.net/w2m0mang/).** 6 | 7 | **What is the expected behavior?** 8 | 9 | **Which versions of Microcosm is affected by this issue?** 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | **What kind of change does this PR introduce?** 7 | 8 | - Bugfix 9 | - Feature 10 | - Code style update 11 | - Refactor 12 | - Build tools 13 | - Documentation 14 | - Other, please describe: 15 | 16 | **Does this PR introduce a breaking change?** 17 | 18 | - Yes 19 | - No 20 | 21 | If yes, please describe the impact and migration path for existing applications: 22 | 23 | **Does this PR fulfill these requirements:** 24 | 25 | 26 | - [ ] All tests are passing 27 | - [ ] `yarn pretty` has been run 28 | - [ ] Commits reference open issues 29 | 30 | **Additional Information** 31 | 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | stats.json 3 | node_modules 4 | *.asm 5 | *.cfg 6 | build 7 | tmp 8 | npm-debug.log 9 | .DS_Store 10 | .babel-cache 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.14.2 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Viget Labs 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build strict min umd es docs 2 | 3 | pretty: 4 | yarn run pretty 5 | 6 | build: build/package.json 7 | @ ./scripts/bundle --out=build 8 | 9 | strict: build/package.json 10 | @ ./scripts/bundle --out=build/strict --strict 11 | @ cp build/package.json build/strict 12 | 13 | min: build/package.json 14 | @ ./scripts/bundle --out=build/min --minify 15 | 16 | umd: build/package.json 17 | @ ./scripts/bundle --out=build/umd --format=umd 18 | 19 | es: build/package.json 20 | @ ./scripts/bundle --out=build/es --format=es 21 | 22 | docs: 23 | @ mkdir -p build 24 | @ cp -r README.md LICENSE build 25 | 26 | build/package.json: package.json 27 | @ mkdir -p build 28 | @ node -p 'p=require("./package");p.private=p.scripts=p.jest=p.devDependencies=undefined;JSON.stringify(p,null,2)' > $@ 29 | 30 | release: clean all 31 | yarn test:prod 32 | npm publish build 33 | 34 | prerelease: clean all 35 | yarn test:prod 36 | npm publish build --tag beta 37 | 38 | bench: build 39 | @ NODE_ENV=production node --expose-gc bench/history-performance 40 | @ NODE_ENV=production node --expose-gc bench/dispatch-performance 41 | @ NODE_ENV=production node --expose-gc bench/push-performance 42 | @ NODE_ENV=production node --expose-gc bench/fork-performance 43 | @ NODE_ENV=production node --expose-gc bench/compare-tree-performance 44 | 45 | clean: 46 | @ rm -rf build/* 47 | 48 | .PHONY: clean bench release prerelease all docs build strict min umd es 49 | -------------------------------------------------------------------------------- /bench/compare-tree-performance.js: -------------------------------------------------------------------------------- 1 | const { Microcosm, update } = require('../build/microcosm') 2 | 3 | let repo = new Microcosm() 4 | 5 | let advance = (x, y) => ({ x, y }) 6 | 7 | let SAMPLES = [25, 50, 100, 200] 8 | let WRITES = 50 9 | 10 | let results = SAMPLES.map(function(SIZE) { 11 | let stats = {} 12 | 13 | repo.addDomain('pixels', { 14 | getInitialState() { 15 | return {} 16 | }, 17 | rotateHue(n) { 18 | return isNaN(n) ? 0 : n + 5 19 | }, 20 | advance(state, { x, y }) { 21 | return update(state, [x, y], this.rotateHue) 22 | }, 23 | register() { 24 | return { 25 | [advance]: this.advance 26 | } 27 | } 28 | }) 29 | 30 | global.gc() 31 | 32 | var memoryBefore = process.memoryUsage().heapUsed 33 | 34 | var then = process.hrtime() 35 | for (let x = 0; x < SIZE; x++) { 36 | for (let y = 0; y < SIZE; y++) { 37 | repo.on(`change:pixels.${x}.${y}`, hue => {}) 38 | } 39 | } 40 | 41 | stats['Dimensions'] = `${SIZE}² = ${SIZE * SIZE}` 42 | 43 | stats['Prep'] = (process.hrtime(then)[1] / 1000000).toFixed() + 'ms' 44 | 45 | then = process.hrtime() 46 | for (var i = 0; i < WRITES; i++) { 47 | let x = Math.floor(Math.random() * SIZE) 48 | let y = Math.floor(Math.random() * SIZE) 49 | 50 | repo.push(advance, x, y) 51 | } 52 | 53 | stats[`Writes (${WRITES})`] = 54 | (process.hrtime(then)[1] / 1000000).toFixed() + 'ms' 55 | 56 | stats['Memory'] = 57 | ((process.memoryUsage().heapUsed - memoryBefore) / 100000).toFixed(2) + 'mb' 58 | 59 | return stats 60 | }) 61 | 62 | require('console.table') 63 | 64 | console.log() 65 | console.table(results) 66 | -------------------------------------------------------------------------------- /bench/dispatch-performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dispatch Performance Benchmark 3 | * The goal of this script is to evaluate if our goal of 10,000 4 | * uniquely folded actions can be done in under 16ms. 5 | * 6 | * This test does not account for hardware diversity. It is a simple 7 | * gut check of "are we fast yet?" 8 | */ 9 | 10 | 'use strict' 11 | 12 | const { Microcosm } = require('../build/microcosm') 13 | 14 | const SIZES = [ 1000, 10000, 50000, 100000 ] 15 | 16 | console.log('\nConducting dispatch benchmark...\n') 17 | 18 | const action = n => n 19 | 20 | const Domain = { 21 | getInitialState: () => 0, 22 | register() { 23 | return { 24 | [action]: (n) => n + 1 25 | } 26 | } 27 | } 28 | 29 | var results = SIZES.map(function (SIZE) { 30 | /** 31 | * Force garbage collection. This is exposed by invoking 32 | * node with --expose-gc. This allows us to record heap usage 33 | * before and after the test to check for memory leakage 34 | */ 35 | global.gc() 36 | 37 | var repo = new Microcosm({ maxHistory: Infinity }) 38 | 39 | /** 40 | * Add the domain at multiple keys. This is a better simulation of actual 41 | * applications. Otherwise, efficiencies are obtained enumerating over 42 | * very few keys. This is never the case in real-world applications. 43 | */ 44 | repo.addDomain('one', Domain) 45 | repo.addDomain('two', Domain) 46 | repo.addDomain('three', Domain) 47 | repo.addDomain('four', Domain) 48 | repo.addDomain('five', Domain) 49 | 50 | var cost = 0 51 | var min = Infinity 52 | var max = -Infinity 53 | 54 | for (var q = 0; q < SIZE; q++) { 55 | var then = process.hrtime() 56 | repo.push(action) 57 | var pass = process.hrtime(then)[1] / 1000000 58 | 59 | min = Math.min(min, pass) 60 | max = Math.max(max, pass) 61 | cost += pass 62 | } 63 | 64 | return { 65 | 'Actions' : SIZE.toLocaleString(), 66 | 'Slowest' : max.toLocaleString() + 'ms', 67 | 'Fastest' : min.toLocaleString() + 'ms', 68 | 'Average' : (cost / SIZE).toLocaleString() + 'ms', 69 | 'Total' : cost.toLocaleString() + 'ms' 70 | } 71 | }) 72 | 73 | /** 74 | * Finally, report our findings. 75 | */ 76 | 77 | require('console.table') 78 | console.table(results) 79 | -------------------------------------------------------------------------------- /bench/fork-performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fork Performance Benchmark 3 | * Measures the cost of forking a repo. This is a good indicator of 4 | * start up time for complicated trees. 5 | */ 6 | 7 | 'use strict' 8 | 9 | const { Microcosm } = require('../build/microcosm') 10 | 11 | const SIZES = [ 1000, 10000, 50000, 100000 ] 12 | 13 | console.log('\nConducting fork benchmark...\n') 14 | 15 | var action = 'test' 16 | 17 | var Domain = { 18 | getInitialState: () => 0, 19 | register() { 20 | return { 21 | [action]: (a, b) => a + b 22 | } 23 | } 24 | } 25 | 26 | var results = SIZES.map(function (SIZE) { 27 | var repo = new Microcosm() 28 | 29 | repo.addDomain('one', Domain) 30 | 31 | var then = process.hrtime() 32 | for (var i = 0; i < SIZE; i++) { 33 | repo.fork().on('change', () => {}) 34 | } 35 | 36 | var setup = process.hrtime(then)[1] / 1000000 37 | 38 | then = process.hrtime() 39 | repo.push(action, 1) 40 | var push = process.hrtime(then)[1] / 1000000 41 | 42 | return { 43 | 'Count' : SIZE.toLocaleString(), 44 | 'Setup' : setup.toLocaleString() + 'ms', 45 | 'Push' : push.toLocaleString() + 'ms' 46 | } 47 | }) 48 | 49 | /** 50 | * Finally, report our findings. 51 | */ 52 | 53 | require('console.table') 54 | 55 | console.table(results) 56 | -------------------------------------------------------------------------------- /bench/history-performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * History Performance Benchmark 3 | */ 4 | 5 | 'use strict' 6 | 7 | const { History } = require('../build/microcosm') 8 | 9 | const SIZES = [ 10 | 1000, 11 | 50000, 12 | 100000 13 | ] 14 | 15 | console.log('\nConducting history benchmark...\n') 16 | 17 | const results = SIZES.map(function (SIZE) { 18 | /** 19 | * Force garbage collection. This is exposed by invoking node with 20 | * --expose-gc. This allows us to record heap usage before and after the test 21 | * to check for memory leakage 22 | */ 23 | global.gc() 24 | 25 | var action = () => {} 26 | var stats = { build: 0, root: 0, merge: 0, size: 0, rollforward: 0, memory: 0 } 27 | var history = new History({ maxHistory: Infinity }) 28 | 29 | var memoryBefore = process.memoryUsage().heapUsed 30 | 31 | /** 32 | * Measure the average append time. 33 | */ 34 | var now = process.hrtime() 35 | for (var k = SIZE; k >= 0; k--) { 36 | history.append(action, 'resolve') 37 | } 38 | var end = process.hrtime(now)[1] 39 | stats.build = (end / 1000000) 40 | 41 | /** 42 | * Measure the time it takes to determine the size of 43 | * the current branch. 44 | */ 45 | var now = process.hrtime() 46 | history.setSize() 47 | var end = process.hrtime(now)[1] 48 | stats.setSize = (end / 1000000) 49 | 50 | var memoryUsage = process.memoryUsage().heapUsed - memoryBefore 51 | 52 | /** 53 | * Measure time to dispose all nodes in the history. This also has 54 | * the side effect of helping to test memory leakage later. 55 | */ 56 | var now = process.hrtime() 57 | history.setLimit(1) 58 | history.reconcile(history.root) 59 | var end = process.hrtime(now)[1] 60 | stats.rollforward = (end / 1000000) 61 | 62 | /** 63 | * Now that the history has been pruned, force garbage collection 64 | * and record the increase in heap usage. 65 | */ 66 | global.gc() 67 | 68 | var memoryAfter = process.memoryUsage().heapUsed 69 | var growth = (1 - (memoryBefore / memoryAfter)) * 100 70 | stats.memory = (memoryAfter - memoryBefore) / 100000 71 | 72 | return { 73 | 'Nodes': SIZE.toLocaleString(), 74 | '::append()': stats.build.toFixed(2) + 'ms', 75 | '::setSize()': stats.setSize.toFixed(2) + 'ms', 76 | '::rollforward()': stats.rollforward.toFixed(2) + 'ms', 77 | 'Total Memory': (memoryUsage / 1000000).toFixed(2) + 'mbs', 78 | 'Memory Growth': stats.memory.toFixed(2) + 'mbs (' + growth.toFixed(2) + '%)' 79 | } 80 | }) 81 | 82 | /** 83 | * Finally, report our findings. 84 | */ 85 | 86 | require('console.table') 87 | 88 | console.table(results) 89 | -------------------------------------------------------------------------------- /bench/push-performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Push Performance Benchmark 3 | * Measures the performance of pushing a single action. 4 | */ 5 | 6 | 'use strict' 7 | 8 | const { Microcosm } = require('../build/microcosm') 9 | 10 | const SIZES = [ 1000, 10000, 50000, 100000 ] 11 | 12 | console.log('\nConducting push benchmark...\n') 13 | 14 | var results = SIZES.map(function (SIZE) { 15 | var repo = new Microcosm() 16 | 17 | var action = function test () {} 18 | action.toString = function () { return 'test' } 19 | 20 | var Domain = { 21 | getInitialState: () => 0, 22 | register() { 23 | return { 24 | test: (n) => n + 1 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Add the domain at multiple keys. This is a better simulation of actual 31 | * applications. Otherwise, efficiencies are obtained enumerating over 32 | * very few keys. This is never the case in real-world applications. 33 | */ 34 | repo.addDomain('one', Domain) 35 | repo.addDomain('two', Domain) 36 | repo.addDomain('three', Domain) 37 | repo.addDomain('four', Domain) 38 | repo.addDomain('five', Domain) 39 | 40 | var then = process.hrtime() 41 | 42 | /** 43 | * Append a given number of actions into history. We use this method 44 | * instead of `::push()` for benchmark setup performance. At the time of writing, 45 | * `push` takes anywhere from 0.5ms to 15ms depending on the sample range. 46 | * This adds up to a very slow boot time! 47 | */ 48 | var startMemory = process.memoryUsage().heapUsed 49 | for (var i = 0; i < SIZE; i++) { 50 | repo.push(action) 51 | } 52 | var endMemory = process.memoryUsage().heapUsed 53 | 54 | var total = process.hrtime(then)[1] / 1000000 55 | var average = total / SIZE 56 | 57 | return { 58 | 'Volume' : SIZE.toLocaleString(), 59 | 'Total' : total.toLocaleString() + 'ms', 60 | 'Average' : average.toLocaleString() + 'ms', 61 | 'Memory Usage' : ((endMemory - startMemory) / 1000000).toFixed(2) + 'mbs' 62 | } 63 | }) 64 | 65 | /** 66 | * Finally, report our findings. 67 | */ 68 | 69 | require('console.table') 70 | 71 | console.table(results) 72 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | 1. [Quick Start](guides/quickstart.md) 4 | 2. [Installation](guides/installation.md) 5 | 3. [Architecture](guides/architecture.md) 6 | 4. [Contributing](guides/contributing.md) 7 | 8 | ## API 9 | 10 | 1. [Microcosm](api/microcosm.md) 11 | 2. [Domains](api/domains.md) 12 | 3. [Actions](api/actions.md) 13 | 4. [Effects](api/effects.md) 14 | 5. [History](api/history.md) 15 | 6. [Immutability Helpers](api/immutability-helpers.md) 16 | 17 | ## Addons 18 | 19 | 1. [Presenter](api/presenter.md) 20 | 2. [ActionForm](api/action-form.md) 21 | 3. [ActionButton](api/action-button.md) 22 | 4. [withSend](api/with-send.md) 23 | 24 | ## Testing 25 | 26 | 1. [Overview](testing/overview.md) 27 | 2. [Domains](testing/domains.md) 28 | 3. [Effects](testing/effects.md) 29 | 4. [Presenters](testing/presenters.md) 30 | 31 | ## Recipes 32 | 33 | 1. [React Router](recipes/react-router.md) 34 | 2. [AJAX](recipes/ajax.md) 35 | 3. [Preact](recipes/preact.md) 36 | 4. [Hydrating State](recipes/hydrating-state.md) 37 | 5. [Batch Updates](recipes/batch-updates.md) 38 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | 1. [Microcosm](./microcosm.md) 4 | 2. [Domains](./domains.md) 5 | 3. [Actions](./actions.md) 6 | 4. [Effects](./effects.md) 7 | 5. [History](./history.md) 8 | 6. [Immutability Helpers](./immutability-helpers.md) 9 | 10 | ## Addons 11 | 12 | 1. [Presenter](./presenter.md) 13 | 2. [ActionForm](./action-form.md) 14 | 3. [ActionButton](./action-button.md) 15 | 4. [withSend](./with-send.md) 16 | -------------------------------------------------------------------------------- /docs/api/with-send.md: -------------------------------------------------------------------------------- 1 | # withSend(Component) 2 | 3 | 1. [Overview](#overview) 4 | 2. [Testing](#testing) 5 | 3. [Accessing the original component](#accessing-the-original-component) 6 | 7 | ## Overview 8 | 9 | A higher order component that can be used to connect a component deep 10 | within the component tree to its associated presenter. This is useful 11 | for requesting work from Presenters where passing down callbacks as 12 | props may otherwise be exhaustive. 13 | 14 | ```javascript 15 | import React from 'react' 16 | import withSend from 'microcosm/addons/with-send' 17 | 18 | const Button = withSend(function({ send }) { 19 | return 20 | }) 21 | ``` 22 | 23 | ```javascript 24 | // within a Presenter: 25 | intercept () { 26 | return { 27 | 'hello-world': this.doSomething 28 | } 29 | } 30 | 31 | doSomething (repo, params) { } 32 | ``` 33 | 34 | ## Testing 35 | 36 | `withSend` relies on the context setup by a Presenter. When testing, 37 | this isn't always available. To work around this, Components wrapped 38 | in `withSend` can accept `send` as a prop: 39 | 40 | ```javascript 41 | import React from 'react' 42 | import {mount} from 'enzyme' 43 | import Button from 'prior-example' 44 | 45 | describe('Button test', function () { 46 | 47 | it('it emits an action when clicked', function() { 48 | expect.assertions(1) 49 | 50 | function assertion (action) { 51 | expect(action).toEqual('hello-world') 52 | } 53 | 54 | mount( 70 | ) 71 | }) 72 | 73 | const WrappedButton = withSend(Button) 74 | 75 | WrappedButton.WrappedComponent // Button 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/guides/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks you for considering a contribution to Microcosm! Contributing to an open source project can be daunting. It shouldn't be, and we are here to help. If you want to get involved but don't know how reach out to us. Contributing to Microcosm ranges from tweeting about it, to helping document it, to writing and improving code. If you want to help, we want you to help. 4 | 5 | ## How you can help: 6 | 7 | We want to make contributing to Microcosm as easy as possible for anyone, here are a few ways you can help: 8 | 9 | 1. [Create an issue on Github](https://github.com/vigetlabs/microcosm/issues). We've provided an issue template that will help us categorize and prioritize the issue. (You will see the template when creating a new issue) 10 | 2. Comment on issues. Let us know your thoughts on [open issues](https://github.com/vigetlabs/microcosm/issues) 11 | 3. Create a pull request (PR) that fixes an issue. We've created a PR template to help you describe what your PR is doing and help us check that it works. PRs can be opened for: 12 | - Documentation 13 | - Improving Microcosm code 14 | - Changes to the site 15 | - Improving or adding examples 16 | - Improving tooling 17 | 4. Spread the word. On twitter you can use the hashtag `#microcosmJS`. You can also tweet @[viget](https://twitter.com/viget) or @[natehunzaker](https://twitter.com/natehunzaker) with your Microcosm questions or comments 18 | 19 | ## Get set up: 20 | 21 | If you are ready to help us out then the look for the `CONTRIBUTING` markdown file on the [Microcosm repo](https://github.com/vigetlabs/microcosm) for all the information you will need to get started. 22 | 23 | > **If you run into any problems let us know through an issue, tweet, or email.** 24 | -------------------------------------------------------------------------------- /docs/guides/index.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | 1. [Quick Start](./quickstart.md) 4 | 2. [Installation](./installation.md) 5 | 3. [Architecture](./architecture.md) 6 | 4. [Contributing](./contributing.md) 7 | -------------------------------------------------------------------------------- /docs/guides/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 1. [Up and running](#up-and-running) 4 | 2. [Strict mode](#strict-mode) 5 | 3. [Optional dependencies](#optional-dependencies) 6 | 7 | ## Up and running 8 | 9 | The easiest way to grab Microcosm is 10 | through [npm](https://www.npmjs.com/package/microcosm): 11 | 12 | ```bash 13 | npm install --save microcosm 14 | ``` 15 | 16 | From there, we recommend using a tool 17 | like [Webpack](https://webpack.js.org/) to bundle your code and pull 18 | in dependencies from `npm`. 19 | 20 | If you have never heard of Webpack, 21 | fantastic! Now's a great time to check it 22 | out. [Webpack's excellent getting started guide](https://webpack.js.org/guides/get-started/) covers 23 | everything you need to know before getting started. 24 | 25 | ## Strict mode 26 | 27 | Microcosm ships with a "strict mode", this is a special version of 28 | Microcosm that includes development-only assertions. These help to 29 | avoid common mistakes and ensure correct usage of Microcosm 30 | APIs. **Assertions do not ship with the standard version of Microcosm. 31 | They are totally opt in.** 32 | 33 | Enable assertions by pointing to `microcosm/strict` in your application: 34 | 35 | ```javascript 36 | import Microcosm from 'microcosm/strict/microcosm.js' 37 | ``` 38 | 39 | However, this is hardly ideal. We recommend using 40 | a 41 | [Webpack alias](https://webpack.js.org/configuration/resolve/#resolve-alias) to 42 | automatically point to the strict version of Microcosm whenever you 43 | import it: 44 | 45 | ```javascript 46 | // webpack.config.js 47 | module.exports = { 48 | // ... 49 | resolve: { 50 | alias: { 51 | microcosm: 'microcosm/strict' 52 | } 53 | } 54 | // ... 55 | } 56 | ``` 57 | 58 | ### How do I remove assertions in production? 59 | 60 | We recommend switching a Webpack alias between environments. Drawing 61 | from the prior example: 62 | 63 | ```javascript 64 | // webpack.config.js 65 | var isProduction = process.env.NODE_ENV !== 'development' 66 | 67 | module.exports = { 68 | // ... 69 | resolve: { 70 | alias: { 71 | microcosm: isProduction ? 'microcosm' : 'microcosm/strict' 72 | } 73 | } 74 | // ... 75 | } 76 | ``` 77 | 78 | ### Optional dependencies 79 | 80 | Microcosm actions can be described as generators. This is a new 81 | JavaScript feature available in JS2015, which does not have wide 82 | support. 83 | 84 | **This is an advanced feature. There is no need to include a polyfill 85 | for generators if you do not use them.** We recommend 86 | using [Babel](https://babeljs.io) with 87 | the [regenerator](https://github.com/facebook/regenerator) polyfill 88 | available 89 | through [babel-polyfill](https://babeljs.io/docs/usage/polyfill/). 90 | -------------------------------------------------------------------------------- /docs/recipes/index.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | 1. [React Router](./react-router.md) 4 | 2. [AJAX](./ajax.md) 5 | 3. [Preact](./preact.md) 6 | 4. [Hydrating State](./hydrating-state.md) 7 | 5. [Batch Updates](./batch-updates.md) 8 | -------------------------------------------------------------------------------- /docs/recipes/preact.md: -------------------------------------------------------------------------------- 1 | # Preact 2 | 3 | Though the `addons` modules that bundle with Microcosm are designed for React, 4 | we also maintain a `microcosm-preact` project for use with [Preact](https://github.com/developit/preact). 5 | 6 | ## Usage 7 | 8 | Install `microcosm-preact` from npm. 9 | 10 | ``` 11 | npm install --save microcosm-preact 12 | ``` 13 | 14 | From there, it can be used exactly like the standard addons: 15 | 16 | ```javascript 17 | import Presenter from 'microcosm-preact/presenter' 18 | 19 | class MyPresenter extends Presenter { 20 | view() { 21 | return

All set

22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/recipes/react-router.md: -------------------------------------------------------------------------------- 1 | # React Router Integration 2 | 3 | For most applications that utilize React Router, having access to 4 | Microcosm is essential for dispatching actions as route handlers enter 5 | the stage. 6 | 7 | We recommend mounting a "root" `Presenter`, framing context such that 8 | child presenters have access to a central Microcosm repo: 9 | 10 | ```javascript 11 | import React from 'react' 12 | import DOM from 'react-dom' 13 | import { Router } from 'react-router' 14 | import createBrowserHistory from 'history/createBrowserHistory' 15 | import { AppContainer } from 'react-hot-loader' 16 | import Application from './views/layout' 17 | 18 | const repo = new Microocsm() 19 | 20 | DOM.render( 21 | 22 | 23 | , 24 | document.getElementById('app') 25 | ) 26 | ``` 27 | 28 | Drawing from the example above, `Application` may look something like: 29 | 30 | ```javascript 31 | import React from 'react' 32 | import Switch from 'react-router/Switch' 33 | import Route from 'react-router/Route' 34 | import Presenter from 'microcosm/addons/presenter' 35 | 36 | class Application extends Presenter { 37 | render() { 38 | return {/* routes */} 39 | } 40 | } 41 | 42 | export default Application 43 | ``` 44 | 45 | For more information, see the [React Router example app](https://github.com/vigetlabs/microcosm/tree/master/examples/react-router). 46 | -------------------------------------------------------------------------------- /docs/testing/index.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | 1. [Overview](./overview.md) 4 | 2. [Domains](./domains.md) 5 | 3. [Effects](./effects.md) 6 | 4. [Presenters](./presenters.md) 7 | -------------------------------------------------------------------------------- /docs/testing/presenters.md: -------------------------------------------------------------------------------- 1 | # Testing Presenters 2 | 3 | Microcosm Presenters, using the `ActionButton`, `Form`, and `withSend` 4 | add-ons, can listen to messages sent from child components deep within a 5 | component tree. This recipe walks through testing that functionality. 6 | 7 | ## The Basic Mechanics 8 | 9 | The communication process relies on 10 | [context](https://facebook.github.io/react/docs/context.html), which can add 11 | some complexity when testing. Fortunately, setting up context with the 12 | [`enzyme`](https://github.com/airbnb/enzyme) testing library makes this 13 | painless. We use the following helper when testing actions to make this process 14 | easy: 15 | 16 | ```javascript 17 | import React from 'react' 18 | import MyForm from 'somewhere' 19 | import { mount } from 'enzyme' 20 | 21 | it('broadcasts an action when submitted', function() { 22 | // You could use any testing library as long as your testing 23 | // environment has spies. We're using Jest here 24 | const send = jest.fn() 25 | 26 | const wrapper = mount(, { 27 | context: { send }, 28 | childContextTypes: { 29 | send: React.PropTypes.func 30 | } 31 | }) 32 | 33 | wrapper.simulate('submit') 34 | 35 | expect(send).lastCalledWith('myAction', { name: 'John Doe' }) 36 | }) 37 | ``` 38 | 39 | ## A Test Helper 40 | 41 | The section above describes using enzyme to frame context. We like to 42 | keep this in a test helper to reduce boilerplate: 43 | 44 | ```javascript 45 | import React from 'react' 46 | 47 | // Any spy library should do, we're using Jest 48 | export default function mockSend(send = jest.fn()) { 49 | send.context = { send } 50 | 51 | send.childContextTypes = { 52 | send: React.PropTypes.func 53 | } 54 | 55 | return send 56 | } 57 | ``` 58 | 59 | Then include the helper when testing: 60 | 61 | ```javascript 62 | import React from 'react' 63 | import MyForm from 'somewhere' 64 | import mockSend from '../helpers/mock-send' 65 | import { mount } from 'enzyme' 66 | 67 | it('broadcasts an action when submitted', function() { 68 | const send = mockSend() 69 | 70 | const wrapper = mount(, send) 71 | 72 | wrapper.simulate('submit') 73 | 74 | expect(send).lastCalledWith('myAction', { name: 'John Doe' }) 75 | }) 76 | ``` 77 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | 1. [What's in here](whats-in-here) 4 | - [Chatbot](chatbot) 5 | - [Painter](painter) 6 | - [ReactRouter](reactrouter) 7 | - [SimpleSVG](simplesvg) 8 | 2. [Run the examples](run-the-examples) 9 | 3. [Project Structure](project-structure) 10 | 11 | ## What's in here 12 | 13 | ### ChatBot 14 | 15 | A messaging client that interacts with a chat bot. This example 16 | demonstrates optimistic updates, asynchronous requests, and loading 17 | states. 18 | 19 | ### Painter 20 | 21 | A simple drawing application. Click tiles across a grid to flip them 22 | between black and white. This example was created to test the 23 | [time-travel debugger for Microcosm](https://github.com/vigetlabs/microcosm-debugger). 24 | 25 | ### ReactRouter 26 | 27 | Demonstrates use of Microcosm with ReactRouter. 28 | 29 | ### SimpleSVG 30 | 31 | An animated SVG example that uses actions to animate the orbiting of 32 | one circle around another. Demonstrates the thunk usage pattern for 33 | actions. 34 | 35 | ## Run the examples 36 | 37 | To run these examples, install project dependencies and execute `npm start` in any of the directories: 38 | 39 | ``` 40 | $ git clone https://github.com/vigetlabs/microcosm 41 | $ cd microcosm 42 | $ npm install 43 | 44 | $ cd examples/simple-svg 45 | $ make 46 | ``` 47 | -------------------------------------------------------------------------------- /examples/canvas/Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | @ $$(npm bin)/webpack-dev-server --config ../webpack.config.js --mode=development 3 | 4 | build: clean 5 | @ $$(npm bin)/webpack -p --config ../webpack.config.js --env=production --mode=production 6 | @ cp public/*.css build/ 7 | 8 | clean: 9 | @ rm -rf build/* 10 | 11 | .PHONY: start build clean 12 | -------------------------------------------------------------------------------- /examples/canvas/app/boot.js: -------------------------------------------------------------------------------- 1 | import Microcosm, { update } from 'microcosm' 2 | import prepareCanvas from './prepare-canvas' 3 | 4 | let repo = new Microcosm({ batch: true }) 5 | 6 | let size = 20 7 | let limit = Infinity 8 | let rows = Math.min((window.innerWidth / size) | 0, limit) 9 | let columns = Math.min((window.innerHeight / size) | 0, limit) 10 | let writes = size * 25 11 | 12 | let advance = (x, y) => ({ x, y }) 13 | 14 | repo.addDomain('pixels', { 15 | getInitialState() { 16 | return {} 17 | }, 18 | rotateHue(n) { 19 | return isNaN(n) ? 0 : n + 5 20 | }, 21 | advance(state, { x, y }) { 22 | return update(state, [x, y], this.rotateHue) 23 | }, 24 | register() { 25 | return { 26 | [advance]: this.advance 27 | } 28 | } 29 | }) 30 | 31 | var canvas = document.createElement('canvas') 32 | var context = canvas.getContext('2d') 33 | 34 | document.body.appendChild(canvas) 35 | 36 | prepareCanvas(canvas) 37 | 38 | context.scale(size, size) 39 | 40 | for (let x = 0; x < rows; x++) { 41 | for (let y = 0; y < columns; y++) { 42 | repo.on(`change:pixels.${x}.${y}`, hue => { 43 | context.fillStyle = `hsl(${hue}, 70%, 60%)` 44 | context.fillRect(x, y, 1, 1) 45 | }) 46 | } 47 | } 48 | 49 | canvas.addEventListener('mousemove', event => { 50 | let x = (event.clientX / size) | 0 51 | let y = (event.clientY / size) | 0 52 | 53 | repo.push(advance, x, y) 54 | }) 55 | 56 | function randomMoves(n = 1) { 57 | while (n-- > 0) { 58 | let x = (Math.random() * rows) | 0 59 | let y = (Math.random() * columns) | 0 60 | 61 | repo.push(advance, x, y) 62 | } 63 | } 64 | 65 | let label = document.getElementById('label') 66 | function updateLabel() { 67 | let events = rows * columns * writes 68 | 69 | label.innerHTML = 70 | `${rows}x${columns} (${size}px grid)` + 71 | `| ${writes} writes/frame` + 72 | `| ${events.toLocaleString()} keys watched/frame` 73 | } 74 | 75 | const SLOW = 1000 / 58 76 | const FAST = 1000 / 60 77 | 78 | function play() { 79 | let last = performance.now() 80 | requestAnimationFrame(function loop() { 81 | let next = performance.now() 82 | let elapsed = next - last 83 | 84 | if (elapsed > SLOW) { 85 | writes-- 86 | } else if (elapsed < FAST) { 87 | writes++ 88 | } 89 | 90 | last = next 91 | 92 | randomMoves(writes) 93 | requestAnimationFrame(loop) 94 | }) 95 | } 96 | 97 | play() 98 | updateLabel() 99 | setInterval(updateLabel, 1500) 100 | -------------------------------------------------------------------------------- /examples/canvas/app/prepare-canvas.js: -------------------------------------------------------------------------------- 1 | export default function prepareCanvas(canvas) { 2 | var context = canvas.getContext('2d') 3 | 4 | // https://www.html5rocks.com/en/tutorials/canvas/hidpi/ 5 | let pixelRatio = window.devicePixelRatio || 1 6 | let backingStore = context.backingStorePixelRatio || 1 7 | let density = pixelRatio / backingStore 8 | 9 | let width = window.innerWidth 10 | let height = window.innerHeight 11 | 12 | canvas.width = width * density 13 | canvas.height = height * density 14 | 15 | canvas.style.height = height + 'px' 16 | canvas.style.width = width + 'px' 17 | 18 | context.scale(density, density) 19 | 20 | context.fillStyle = '#eee' 21 | context.fillRect(0, 0, width, height) 22 | context.font = '16px Helvetica' 23 | } 24 | -------------------------------------------------------------------------------- /examples/chatbot/Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | @ $$(npm bin)/webpack-dev-server --config ./webpack.config.js --mode=development 3 | 4 | .PHONY: start 5 | -------------------------------------------------------------------------------- /examples/chatbot/README.md: -------------------------------------------------------------------------------- 1 | # Chatbot Example 2 | 3 | A messaging client that interacts with a chat bot. This example 4 | demonstrates optimistic updates, asynchronous requests, and loading 5 | states. 6 | 7 | ## Setup 8 | 9 | ``` 10 | npm install 11 | make start 12 | open http://localhost:4000 13 | ``` 14 | 15 | ## How it works 16 | 17 | This example demonstrates using the current state of actions to 18 | provide optimistic updates and loading states. When an action is 19 | pushed into a Microcosm, this example uses the `open`, `done`, and 20 | `error` states to display useful information to the user. 21 | 22 | The best place to see where this happens is within 23 | [`./app/domains/message.js`](./app/domains/message.js), where the 24 | domain's registration method listens to specific action states: 25 | 26 | ```javascript 27 | const Messages = { 28 | // .. 29 | register() { 30 | return { 31 | [send.open]: Messages.addLoading, 32 | [send.done]: Messages.add, 33 | [send.error]: Messages.addError 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | ## What are action states? 40 | 41 | Whenever an action creator is pushed into Microcosm, it creates an 42 | action object to represent the resolution of that action 43 | creator. Domains can hook into the different states of that action as the 44 | action creator resolves. These states _loosely_ follow the 45 | `readyState` property of `XMLHTTPRequest`: 46 | 47 | 1. **unset**: Nothing has happened yet. The action creator has not 48 | started. 49 | 2. **open**: The action creator has started working, such as the opening 50 | of an XHR request, however no response has been given. 51 | 3. **loading**: The action creator is partially complete, such as 52 | downloading a response from a server. 53 | 4. **done**: The action creator has resolved. 54 | 5. **cancelled**: The action was cancelled, like if an XHR request is 55 | aborted. 56 | -------------------------------------------------------------------------------- /examples/chatbot/app/actions/messages.js: -------------------------------------------------------------------------------- 1 | import { post } from 'axios' 2 | 3 | export function send(params) { 4 | return post('/message', params).then(response => response.data) 5 | } 6 | 7 | export function receive(message) { 8 | return message 9 | } 10 | -------------------------------------------------------------------------------- /examples/chatbot/app/boot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DOM from 'react-dom' 3 | import Repo from './repo' 4 | import Chat from './views/chat' 5 | 6 | const repo = new Repo({ debug: true, maxHistory: Infinity }) 7 | 8 | function render() { 9 | DOM.render(, document.getElementById('app')) 10 | } 11 | 12 | render() 13 | 14 | if (module.hot) { 15 | module.hot.accept(render) 16 | } 17 | -------------------------------------------------------------------------------- /examples/chatbot/app/domains/messages.js: -------------------------------------------------------------------------------- 1 | import Message from '../records/message' 2 | import { send } from '../actions/messages' 3 | 4 | const Messages = { 5 | getInitialState() { 6 | return [Message({ user: 'Eliza', message: "What's new with you?" })] 7 | }, 8 | 9 | add(state, items) { 10 | const messages = [].concat(items).map(Message) 11 | 12 | return state.concat(messages) 13 | }, 14 | 15 | addLoading(state, params) { 16 | return this.add(state, { ...params, user: 'You', pending: true }) 17 | }, 18 | 19 | addError(state, params) { 20 | return this.add(state, { ...params, error: true }) 21 | }, 22 | 23 | register() { 24 | return { 25 | [send.open]: Messages.addLoading, 26 | [send.done]: Messages.add, 27 | [send.error]: Messages.addError 28 | } 29 | } 30 | } 31 | 32 | export default Messages 33 | -------------------------------------------------------------------------------- /examples/chatbot/app/records/message.js: -------------------------------------------------------------------------------- 1 | import uid from 'uid' 2 | 3 | function Message({ 4 | id = uid(), 5 | message, 6 | user, 7 | error = false, 8 | pending = false, 9 | time = new Date() 10 | }) { 11 | return { id, message, user, error, pending, time } 12 | } 13 | 14 | export default Message 15 | -------------------------------------------------------------------------------- /examples/chatbot/app/repo.js: -------------------------------------------------------------------------------- 1 | import Microcosm from 'microcosm' 2 | import Messages from './domains/messages' 3 | 4 | class Repo extends Microcosm { 5 | setup() { 6 | this.addDomain('messages', Messages) 7 | } 8 | } 9 | 10 | export default Repo 11 | -------------------------------------------------------------------------------- /examples/chatbot/app/views/chat.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Presenter from 'microcosm/addons/presenter' 3 | import Messenger from './parts/messenger' 4 | 5 | export default class ChatPresenter extends Presenter { 6 | getModel() { 7 | return { 8 | messages: state => state.messages 9 | } 10 | } 11 | 12 | render() { 13 | return 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/chatbot/app/views/parts/announcer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Announcer({ user, message }) { 4 | return ( 5 |
6 | {user} said: {message} 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /examples/chatbot/app/views/parts/conversation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Message from './message' 3 | 4 | class Conversation extends React.PureComponent { 5 | static defaultProps = { 6 | messages: [] 7 | } 8 | 9 | componentDidUpdate() { 10 | let el = this.refs.list 11 | 12 | el.scrollTop = el.scrollHeight 13 | } 14 | 15 | getMessage(message) { 16 | return 17 | } 18 | 19 | render() { 20 | const { messages } = this.props 21 | 22 | return ( 23 |
    24 | {messages.map(this.getMessage, this)} 25 |
26 | ) 27 | } 28 | } 29 | 30 | export default Conversation 31 | -------------------------------------------------------------------------------- /examples/chatbot/app/views/parts/message.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Message extends React.PureComponent { 4 | static defaultProps = { 5 | user: '', 6 | message: '', 7 | pending: false, 8 | time: new Date() 9 | } 10 | 11 | render() { 12 | const { user, time, message, error, pending } = this.props 13 | 14 | const status = pending ? 'sending...' : error ? '✖' : '✔' 15 | const safeTime = new Date(time) 16 | 17 | return ( 18 |
  • 19 | {user} 20 | 23 |
    {message}
    24 |
  • 25 | ) 26 | } 27 | } 28 | 29 | export default Message 30 | -------------------------------------------------------------------------------- /examples/chatbot/app/views/parts/messenger.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Announcer from './announcer' 3 | import Conversation from './conversation' 4 | import Prompt from './prompt' 5 | 6 | export default function Messenger({ messages = [] }) { 7 | var toSay = messages.filter(m => m.user !== 'You').pop() 8 | 9 | return ( 10 |
    11 | 12 | 13 | 14 |
    15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /examples/chatbot/app/views/parts/prompt.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ActionForm from 'microcosm/addons/action-form' 3 | 4 | import { send } from '../../actions/messages' 5 | 6 | const onSubmit = event => event.target.reset() 7 | 8 | export default function Prompt() { 9 | return ( 10 | 11 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/chatbot/lib/chat.js: -------------------------------------------------------------------------------- 1 | var Eliza = require('elizabot') 2 | var uid = require('uid') 3 | 4 | var COMMANDS = { 5 | error(bot, message) { 6 | return { id: uid(), error: true, user: 'You', message } 7 | }, 8 | 9 | reply(bot, message) { 10 | return [ 11 | { id: uid(), user: 'You', message: message }, 12 | { id: uid(), user: 'Eliza', message: bot.transform(message) } 13 | ] 14 | }, 15 | 16 | unknown(bot, message) { 17 | return { 18 | id: uid(), 19 | user: 'System', 20 | message: `Unknown command “${message}”`, 21 | error: true 22 | } 23 | } 24 | } 25 | 26 | exports.start = function() { 27 | return new Eliza() 28 | } 29 | 30 | exports.greet = function(bot) { 31 | return { id: uid(), user: 'Eliza', message: bot.getInitial() } 32 | } 33 | 34 | exports.parse = function parse(bot, message) { 35 | var command = message.match(/^\/(\w+)\s*(.*)/) 36 | 37 | if (command) { 38 | var action = command[1] 39 | var options = command[2] 40 | 41 | if (action in COMMANDS) { 42 | return COMMANDS[action](bot, options) 43 | } else { 44 | return COMMANDS.unknown(bot, action) 45 | } 46 | } 47 | 48 | return COMMANDS.reply(bot, message) 49 | } 50 | -------------------------------------------------------------------------------- /examples/chatbot/lib/server.js: -------------------------------------------------------------------------------- 1 | const Chat = require('./chat') 2 | const bodyParser = require('body-parser') 3 | 4 | module.exports = function(app) { 5 | let bot = Chat.start() 6 | 7 | app.use(bodyParser.json()) 8 | 9 | app.post('/message', function(req, res) { 10 | // Simulate latency 11 | setTimeout(function() { 12 | res.send(Chat.parse(bot, req.body.message)) 13 | }, 500 + Math.random() * 500) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /examples/chatbot/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const server = require('./lib/server') 4 | const base = require('../webpack.config') 5 | 6 | module.exports = function(env) { 7 | let config = base(env) 8 | 9 | // Setup the webpack dev server to include our API endpoints 10 | config.devServer.before = server 11 | 12 | return config 13 | } 14 | -------------------------------------------------------------------------------- /examples/painter/Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | @ $$(npm bin)/webpack-dev-server --config ../webpack.config.js --mode=development 3 | 4 | build: clean 5 | @ $$(npm bin)/webpack -p --config ../webpack.config.js --env=production --mode=production 6 | @ cp public/*.css build/ 7 | 8 | clean: 9 | @ rm -rf build/* 10 | 11 | .PHONY: start build clean 12 | -------------------------------------------------------------------------------- /examples/painter/README.md: -------------------------------------------------------------------------------- 1 | # Painter Example 2 | 3 | A simple drawing application. Click tiles across a grid to flip them 4 | between black and white. This example was created to test the 5 | [time-travel debugger for Microcosm](https://github.com/vigetlabs/microcosm-debugger). 6 | 7 | A pixel-painting application to showcase some of Microcosm's capabilities around 8 | action history using [microcosm-debugger](https://github.com/vigetlabs/microcosm-debugger). 9 | 10 | ## Setup 11 | 12 | ``` 13 | npm install 14 | make start 15 | open http://localhost:4000 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/painter/app/actions/pixels.js: -------------------------------------------------------------------------------- 1 | export function paint(point) { 2 | return point 3 | } 4 | -------------------------------------------------------------------------------- /examples/painter/app/boot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DOM from 'react-dom' 3 | import Debugger from 'microcosm-debugger' 4 | import { AppContainer } from 'react-hot-loader' 5 | import Repo from './repo' 6 | import Workspace from './views/workspace' 7 | 8 | let repo = new Repo({ maxHistory: Infinity }) 9 | 10 | Debugger(repo) 11 | 12 | function render() { 13 | DOM.render( 14 | 15 | 16 | , 17 | document.querySelector('#app') 18 | ) 19 | } 20 | 21 | render() 22 | 23 | if (module.hot) { 24 | module.hot.accept('./views/workspace', render) 25 | } 26 | -------------------------------------------------------------------------------- /examples/painter/app/domains/pixels.js: -------------------------------------------------------------------------------- 1 | import { update } from 'microcosm' 2 | import { paint } from '../actions/pixels' 3 | 4 | const Pixels = { 5 | getInitialState() { 6 | const matrix = [] 7 | 8 | while (matrix.length < 15) { 9 | const row = [] 10 | 11 | while (row.length < 15) { 12 | row.push(0) 13 | } 14 | 15 | matrix.push(row) 16 | } 17 | 18 | return matrix 19 | }, 20 | 21 | flip(pixels, { x, y }) { 22 | return update(pixels, [y, x], val => (val ? 0 : 1), 0) 23 | }, 24 | 25 | register() { 26 | return { 27 | [paint]: this.flip 28 | } 29 | } 30 | } 31 | 32 | export default Pixels 33 | -------------------------------------------------------------------------------- /examples/painter/app/repo.js: -------------------------------------------------------------------------------- 1 | import Microcosm from 'microcosm' 2 | import Pixels from './domains/pixels' 3 | 4 | export default class Repo extends Microcosm { 5 | setup() { 6 | this.addDomain('pixels', Pixels) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/painter/app/views/canvas.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Row from './row' 3 | 4 | export default function Canvas({ pixels, height, width, send }) { 5 | const scaleX = width / pixels[0].length 6 | const scaleY = height / pixels.length 7 | const transform = `scale(${scaleX}, ${scaleY})` 8 | 9 | return ( 10 | 11 | 17 | {pixels.map((row, y) => ( 18 | 19 | ))} 20 | 21 | 22 | ) 23 | } 24 | 25 | Canvas.defaultProps = { 26 | pixels: [], 27 | height: 400, 28 | width: 400 29 | } 30 | -------------------------------------------------------------------------------- /examples/painter/app/views/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import withSend from 'microcosm/addons/with-send' 3 | 4 | import { paint } from '../actions/pixels' 5 | 6 | function Cell({ x, y, active, onClick }) { 7 | const color = active ? 'black' : 'white' 8 | 9 | return ( 10 | 11 | ) 12 | } 13 | 14 | export default withSend(function Row({ cells, y, send }) { 15 | return ( 16 | 17 | {cells.map((active, x) => ( 18 | send(paint, { x, y })} 24 | /> 25 | ))} 26 | 27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /examples/painter/app/views/workspace.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Presenter from 'microcosm/addons/presenter' 3 | import Canvas from './canvas' 4 | 5 | class Workspace extends Presenter { 6 | getModel() { 7 | return { 8 | pixels: state => state.pixels 9 | } 10 | } 11 | 12 | render() { 13 | return 14 | } 15 | } 16 | 17 | export default Workspace 18 | -------------------------------------------------------------------------------- /examples/painter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Painter 6 | 7 | 8 | 9 |
    10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/painter/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #555; 3 | font-family: sans-serif; 4 | margin: 2em; 5 | width: 100%; 6 | } 7 | 8 | rect, 9 | circle { 10 | transition: 0.3s fill, 0.3s r; 11 | cursor: pointer; 12 | } 13 | 14 | svg { 15 | border: 1px solid #ccc; 16 | } 17 | 18 | svg + svg { 19 | border-left: 0; 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-router/Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | @ $$(npm bin)/webpack-dev-server --config ../webpack.config.js --mode=development 3 | 4 | build: clean 5 | @ $$(npm bin)/webpack -p --config ../webpack.config.js --env=production --mode=production 6 | @ cp public/*.css build/ 7 | 8 | clean: 9 | @ rm -rf build/* 10 | 11 | .PHONY: start build clean 12 | -------------------------------------------------------------------------------- /examples/react-router/README.md: -------------------------------------------------------------------------------- 1 | # React Router Example 2 | 3 | A simple Todo app demonstrating the use of Microcosm with React Router. 4 | 5 | ## Setup 6 | 7 | ``` 8 | npm install 9 | make start 10 | open http://localhost:3000 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/react-router/app/actions/items.js: -------------------------------------------------------------------------------- 1 | import Items from '../domains/items' 2 | 3 | export function addItem(params) { 4 | return function*(repo) { 5 | yield repo.push(Items.create, params) 6 | } 7 | } 8 | 9 | export function removeItem(id) { 10 | return function*(repo) { 11 | yield repo.push(Items.destroy, id) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-router/app/actions/lists.js: -------------------------------------------------------------------------------- 1 | import Lists from '../domains/lists' 2 | import { visit } from './routing' 3 | 4 | export function addList(params) { 5 | return function*(repo) { 6 | let list = yield repo.push(Lists.create, params) 7 | 8 | yield repo.push(visit, `/lists/${list.id}`) 9 | } 10 | } 11 | 12 | export function removeList(id) { 13 | return function*(repo) { 14 | yield repo.push(Lists.destroy, id) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/react-router/app/actions/routing.js: -------------------------------------------------------------------------------- 1 | export function visit(path) { 2 | return path 3 | } 4 | -------------------------------------------------------------------------------- /examples/react-router/app/boot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DOM from 'react-dom' 3 | import { Router } from 'react-router' 4 | import createBrowserHistory from 'history/createBrowserHistory' 5 | import Repo from './repo' 6 | import Application from './views/application' 7 | 8 | // We're creating a browser history first, so that we can pass it 9 | // into Microcosm. This lets us take advantage of the router when 10 | // dispatching actions. 11 | // 12 | // See ./effects/routing.js 13 | const browserHistory = createBrowserHistory() 14 | 15 | const repo = new Repo({ browserHistory }) 16 | 17 | DOM.render( 18 | 19 | 20 | , 21 | document.getElementById('app') 22 | ) 23 | -------------------------------------------------------------------------------- /examples/react-router/app/domains/domain.js: -------------------------------------------------------------------------------- 1 | class Domain { 2 | getInitialState() { 3 | return [] 4 | } 5 | 6 | add(items, params) { 7 | return items.concat(params) 8 | } 9 | 10 | remove(items, unwanted) { 11 | return items.filter(i => i.id !== unwanted) 12 | } 13 | 14 | removeBy(key) { 15 | return (items, value) => { 16 | return items.filter(item => item[key] !== value) 17 | } 18 | } 19 | } 20 | 21 | export default Domain 22 | -------------------------------------------------------------------------------- /examples/react-router/app/domains/items.js: -------------------------------------------------------------------------------- 1 | import uid from 'uid' 2 | import Lists from './lists' 3 | import Domain from './domain' 4 | 5 | class Items extends Domain { 6 | static create(params) { 7 | return { id: uid(), ...params } 8 | } 9 | 10 | static destroy(id) { 11 | return id 12 | } 13 | 14 | register() { 15 | return { 16 | [Items.create]: this.add, 17 | [Items.destroy]: this.remove, 18 | [Lists.destroy]: this.removeBy('list') 19 | } 20 | } 21 | } 22 | 23 | export default Items 24 | -------------------------------------------------------------------------------- /examples/react-router/app/domains/lists.js: -------------------------------------------------------------------------------- 1 | import uid from 'uid' 2 | import Domain from './domain' 3 | 4 | class Lists extends Domain { 5 | static create(params) { 6 | return { id: uid(), ...params } 7 | } 8 | 9 | static destroy(id) { 10 | return id 11 | } 12 | 13 | register() { 14 | return { 15 | [Lists.create]: this.add, 16 | [Lists.destroy]: this.remove 17 | } 18 | } 19 | } 20 | 21 | export default Lists 22 | -------------------------------------------------------------------------------- /examples/react-router/app/effects/routing.js: -------------------------------------------------------------------------------- 1 | import { visit } from '../actions/routing' 2 | 3 | class Routing { 4 | setup(repo, { history }) { 5 | this.history = history 6 | } 7 | 8 | visitRoute(repo, route) { 9 | this.history.push(route) 10 | } 11 | 12 | register() { 13 | return { 14 | [visit]: this.visitRoute 15 | } 16 | } 17 | } 18 | 19 | export default Routing 20 | -------------------------------------------------------------------------------- /examples/react-router/app/models/lists.js: -------------------------------------------------------------------------------- 1 | import { set } from 'microcosm' 2 | 3 | export class ListsWithCounts { 4 | call(_presenter, state) { 5 | const { lists, items } = state 6 | 7 | return lists.map(function(list) { 8 | let count = items.filter(i => i.list === list.id).length 9 | 10 | return set(list, 'count', count) 11 | }) 12 | } 13 | } 14 | 15 | export class List { 16 | constructor(id) { 17 | this.id = id 18 | } 19 | 20 | call(_presenter, state) { 21 | const { lists } = state 22 | 23 | return lists.find(list => list.id === this.id) 24 | } 25 | } 26 | 27 | export class ListItems { 28 | constructor(id) { 29 | this.id = id 30 | } 31 | 32 | call(_presenter, state) { 33 | const { items } = state 34 | 35 | return items.filter(item => item.list === this.id) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/react-router/app/repo.js: -------------------------------------------------------------------------------- 1 | import Microcosm from 'microcosm' 2 | import Items from './domains/items' 3 | import Lists from './domains/lists' 4 | import Routing from './effects/routing' 5 | 6 | export default class Repo extends Microcosm { 7 | setup({ browserHistory }) { 8 | this.addDomain('lists', Lists) 9 | this.addDomain('items', Items) 10 | 11 | this.addEffect(Routing, { history: browserHistory }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-router/app/views/application.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Switch from 'react-router/Switch' 3 | import Route from 'react-router/Route' 4 | import Presenter from 'microcosm/addons/presenter' 5 | import ListIndex from './lists/index' 6 | import ListShow from './lists/show' 7 | import NotFound from './errors/notfound' 8 | 9 | class Application extends Presenter { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | } 20 | 21 | export default Application 22 | -------------------------------------------------------------------------------- /examples/react-router/app/views/errors/notfound.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | export default function NotFound({ resource = 'Page' }) { 5 | return ( 6 |
    7 |
    8 |

    {resource} Not Found

    9 |
    10 | 11 |
    12 |

    13 | It is possible this {resource.toLowerCase()} was removed or never 14 | existed. 15 |

    16 |

    17 | Try starting over from the beginning 18 |

    19 |
    20 |
    21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-router/app/views/lists/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Presenter from 'microcosm/addons/presenter' 3 | import ListForm from './parts/list-form' 4 | import ListList from './parts/list-list' 5 | import { ListsWithCounts } from '../../models/lists' 6 | 7 | export default class ListIndex extends Presenter { 8 | getModel() { 9 | return { 10 | lists: new ListsWithCounts() 11 | } 12 | } 13 | 14 | render() { 15 | const { lists } = this.model 16 | 17 | return ( 18 |
    19 |
    20 |
    21 |

    Todos

    22 |
    23 |
    24 | 25 |
    26 | 30 | 31 | 32 |
    33 |
    34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/react-router/app/views/lists/parts/item-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ActionForm from 'microcosm/addons/action-form' 3 | import { addItem } from '../../../actions/items' 4 | 5 | class ItemForm extends React.PureComponent { 6 | state = { 7 | name: '' 8 | } 9 | 10 | reset = () => { 11 | this.setState({ name: '' }) 12 | } 13 | 14 | setName = e => { 15 | this.setState({ name: e.target.value }) 16 | } 17 | 18 | render() { 19 | const { name } = this.state 20 | 21 | return ( 22 | 23 | 24 | 25 |
    26 | 27 | 34 |
    35 | 36 | 37 |
    38 | ) 39 | } 40 | } 41 | 42 | export default ItemForm 43 | -------------------------------------------------------------------------------- /examples/react-router/app/views/lists/parts/item-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ActionButton from 'microcosm/addons/action-button' 3 | import { removeItem } from '../../../actions/items' 4 | 5 | function Item({ id, name }) { 6 | return ( 7 |
  • 8 | {name} 9 | 10 | Delete 11 | 12 |
  • 13 | ) 14 | } 15 | 16 | function Empty() { 17 | return

    No items

    18 | } 19 | 20 | export default function ItemList({ items }) { 21 | return items.length ?
      {items.map(Item)}
    : 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-router/app/views/lists/parts/list-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ActionForm from 'microcosm/addons/action-form' 3 | import { addList } from '../../../actions/lists' 4 | 5 | class ListForm extends React.PureComponent { 6 | state = { 7 | name: '' 8 | } 9 | 10 | reset = () => { 11 | this.setState({ name: '' }) 12 | } 13 | 14 | setName = e => { 15 | this.setState({ name: e.target.value }) 16 | } 17 | 18 | render() { 19 | const { name } = this.state 20 | 21 | return ( 22 | 23 |
    24 | 25 | 32 |
    33 | 34 | 35 |
    36 | ) 37 | } 38 | } 39 | 40 | export default ListForm 41 | -------------------------------------------------------------------------------- /examples/react-router/app/views/lists/parts/list-list.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ActionButton from 'microcosm/addons/action-button' 3 | import { Link } from 'react-router-dom' 4 | import { removeList } from '../../../actions/lists' 5 | 6 | function List({ id, name, count }) { 7 | return ( 8 |
  • 9 | 10 | {name} ({count}) 11 | 12 | 13 | Delete 14 | 15 |
  • 16 | ) 17 | } 18 | 19 | function Empty() { 20 | return

    No lists

    21 | } 22 | 23 | export default function ListList({ items = [] }) { 24 | return items.length ?
      {items.map(List)}
    : 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-router/app/views/lists/show.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Presenter from 'microcosm/addons/presenter' 3 | import NotFound from '../errors/notfound' 4 | import ItemForm from './parts/item-form' 5 | import ItemList from './parts/item-list' 6 | import { Link } from 'react-router-dom' 7 | import { List, ListItems } from '../../models/lists' 8 | 9 | class ListShow extends Presenter { 10 | getModel({ match }) { 11 | let { id } = match.params 12 | 13 | return { 14 | list: new List(id), 15 | items: new ListItems(id) 16 | } 17 | } 18 | 19 | render() { 20 | const { list, items } = this.model 21 | 22 | if (!list) { 23 | return 24 | } 25 | 26 | return ( 27 |
    28 |
    29 |
    30 |

    31 | Lists › {list.name} 32 |

    33 |
    34 |
    35 | 36 |
    37 | 41 | 42 | 43 |
    44 |
    45 | ) 46 | } 47 | } 48 | 49 | export default ListShow 50 | -------------------------------------------------------------------------------- /examples/react-router/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Advanced Microcosm Example 6 | 7 | 8 | 9 |
    10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/react-router/public/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | font-family: Roboto, sans-serif; 5 | -webkit-text-size-adjust: 100%; 6 | } 7 | 8 | h1, 9 | h2, 10 | h3, 11 | h4 { 12 | margin: 0; 13 | } 14 | 15 | p { 16 | margin: 8px 0; 17 | } 18 | 19 | a { 20 | text-decoration: none; 21 | color: inherit; 22 | } 23 | 24 | a:focus, 25 | a:hover { 26 | color: #e61875; 27 | } 28 | 29 | .text-display { 30 | font-size: 34px; 31 | font-weight: 400; 32 | line-height: 52px; 33 | } 34 | 35 | .text-body { 36 | font-size: 14px; 37 | font-weight: 400; 38 | line-height: 25px; 39 | } 40 | 41 | .textfield { 42 | padding: 8px 16px; 43 | } 44 | 45 | .btn { 46 | background: transparent; 47 | border: 0; 48 | cursor: pointer; 49 | font-family: inherit; 50 | padding: 12px 16px; 51 | font-weight: bold; 52 | text-transform: uppercase; 53 | } 54 | 55 | .btn:hover, 56 | .btn:focus { 57 | color: #e61875; 58 | } 59 | 60 | input:not([type='submit']) { 61 | border: 0; 62 | border-bottom: 1px solid #d9d9d9; 63 | width: 100%; 64 | } 65 | 66 | .header { 67 | background: #3f51b5; 68 | color: white; 69 | padding: 48px 0 16px; 70 | } 71 | 72 | .aside { 73 | background: white; 74 | border-radius: 1px; 75 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 76 | margin: 16px 0 0; 77 | padding-bottom: 8px; 78 | } 79 | 80 | @media all and (min-width: 768px) { 81 | .aside { 82 | float: right; 83 | margin: -48px 0 0; 84 | width: 30%; 85 | } 86 | } 87 | 88 | .subhead { 89 | font-size: 10px; 90 | font-weight: 400; 91 | line-height: 32px; 92 | opacity: 0.54; 93 | padding: 8px 16px 4px; 94 | text-transform: uppercase; 95 | } 96 | 97 | .container { 98 | margin: 0 auto; 99 | max-width: 800px; 100 | padding: 0 16px; 101 | } 102 | 103 | label { 104 | font-weight: bold; 105 | display: block; 106 | margin-bottom: 8px; 107 | font-size: 13px; 108 | } 109 | 110 | .list { 111 | list-style: none; 112 | padding: 0; 113 | } 114 | 115 | .list li { 116 | padding: 0 16px 0 0; 117 | line-height: 38px; 118 | overflow: hidden; 119 | } 120 | 121 | .list li + li { 122 | border-top: 1px solid #d9d9d9; 123 | } 124 | 125 | .list .btn { 126 | float: right; 127 | } 128 | 129 | @media all and (min-width: 768px) { 130 | .list { 131 | max-width: 66%; 132 | } 133 | } 134 | 135 | .warn-no-js { 136 | background: #fc4; 137 | color: #431; 138 | font-size: 13px; 139 | left: 0; 140 | margin: 0; 141 | padding: 12px; 142 | position: fixed; 143 | text-align: center; 144 | top: 0; 145 | width: 100%; 146 | } 147 | -------------------------------------------------------------------------------- /examples/simple-svg/Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | @ $$(npm bin)/webpack-dev-server --config ../webpack.config.js --mode=development 3 | 4 | build: clean 5 | @ $$(npm bin)/webpack -p --config ../webpack.config.js --env=production --mode=production 6 | @ cp public/*.css build/ 7 | 8 | clean: 9 | @ rm -rf build/* 10 | 11 | .PHONY: start build clean 12 | -------------------------------------------------------------------------------- /examples/simple-svg/README.md: -------------------------------------------------------------------------------- 1 | # SVG Animation Example 2 | 3 | An animated SVG example that uses actions to animate the orbiting of 4 | one circle around another. Demonstrates the thunk usage pattern for 5 | actions. 6 | 7 | ## Setup 8 | 9 | ``` 10 | npm install 11 | make start 12 | open http://localhost:4000 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/simple-svg/app/actions/animate.js: -------------------------------------------------------------------------------- 1 | function randomColor() { 2 | return '#' + Math.floor(Math.random() * 16777215).toString(16) 3 | } 4 | 5 | export function animate(time, duration) { 6 | let goal = time + duration 7 | let color = randomColor() 8 | 9 | return function loop(action) { 10 | time += 16 11 | 12 | if (time > goal) { 13 | action.resolve({ color, time }) 14 | } else { 15 | action.update({ color, time }) 16 | requestAnimationFrame(() => loop(action)) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/simple-svg/app/boot.js: -------------------------------------------------------------------------------- 1 | import DOM from 'react-dom' 2 | import React from 'react' 3 | import Microcosm from 'microcosm' 4 | import Circle from './domains/circle' 5 | import Logo from './views/logo' 6 | import { animate } from './actions/animate' 7 | 8 | let repo = new Microcosm() 9 | let el = document.getElementById('app') 10 | 11 | repo.addDomain('circle', Circle) 12 | 13 | repo.on('change', function(state) { 14 | DOM.render(, el) 15 | }) 16 | 17 | function loop({ time = Date.now() } = {}) { 18 | repo.push(animate, time, 1000).onDone(loop) 19 | } 20 | 21 | loop() 22 | 23 | if (module.hot) { 24 | module.hot.accept() 25 | } 26 | -------------------------------------------------------------------------------- /examples/simple-svg/app/domains/circle.js: -------------------------------------------------------------------------------- 1 | import { animate } from '../actions/animate' 2 | 3 | const Circle = { 4 | getInitialState() { 5 | return Circle.set(null, { color: 'orange', time: Date.now() }) 6 | }, 7 | 8 | set(_, { color, time }) { 9 | let sin = Math.sin(time / 200) 10 | let cos = Math.cos(time / 200) 11 | 12 | return { 13 | color: color, 14 | cx: 50 * sin, 15 | cy: 35 * cos, 16 | r: 12 + 8 * cos 17 | } 18 | }, 19 | 20 | register() { 21 | return { 22 | [animate.loading]: Circle.set, 23 | [animate.done]: Circle.set 24 | } 25 | } 26 | } 27 | 28 | export default Circle 29 | -------------------------------------------------------------------------------- /examples/simple-svg/app/views/logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Logo({ circle }) { 4 | const { cx, cy, r, color } = circle 5 | 6 | return ( 7 | 8 | Microcosm SVG Chart Example 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple-svg/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple SVG 6 | 7 | 8 | 9 |
    10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/simple-svg/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #555; 3 | font-family: sans-serif; 4 | margin: 0; 5 | width: 100%; 6 | } 7 | 8 | svg { 9 | border: 0; 10 | display: block; 11 | margin: 0 auto; 12 | } 13 | 14 | text { 15 | fill: currentColor; 16 | } 17 | 18 | circle { 19 | transition: 0.3s fill; 20 | } 21 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Follows 3 | * https://webpack.js.org/guides/hmr-react/ 4 | */ 5 | 6 | const HtmlWebpackPlugin = require('html-webpack-plugin') 7 | const { resolve } = require('path') 8 | 9 | const PORT = process.env.PORT || 3000 10 | 11 | module.exports = function(env) { 12 | let isDev = env !== 'production' 13 | let root = process.cwd() 14 | 15 | process.env.BABEL_ENV = isDev ? 'development' : 'production' 16 | 17 | return { 18 | context: root, 19 | 20 | devtool: isDev ? 'cheap-module-inline-source-map' : 'source-map', 21 | 22 | entry: { 23 | application: ['./app/boot.js'] 24 | }, 25 | 26 | output: { 27 | filename: '[name].[hash].js', 28 | path: resolve(root, 'build'), 29 | publicPath: '/' 30 | }, 31 | 32 | resolve: { 33 | alias: { 34 | microcosm$: resolve(__dirname, '../src/index.js'), 35 | microcosm: resolve(__dirname, '../src/') 36 | } 37 | }, 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.js$/, 43 | loader: 'babel-loader', 44 | exclude: /node_modules/, 45 | options: { 46 | plugins: [['transform-runtime', { polyfill: false }]] 47 | } 48 | } 49 | ] 50 | }, 51 | 52 | plugins: [ 53 | new HtmlWebpackPlugin({ 54 | inject: true, 55 | template: resolve(root, 'public/index.html') 56 | }) 57 | ], 58 | 59 | devServer: { 60 | contentBase: resolve(root, 'public'), 61 | publicPath: '/', 62 | compress: true, 63 | historyApiFallback: true, 64 | port: PORT 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /flow/command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /** 6 | * Commands generate actions. When `repo.push` is 7 | * invoked, it processes a given command, returning an action to 8 | * represent the resolution of that command. 9 | */ 10 | declare type Command = Function | string 11 | 12 | declare type Tagged = { 13 | name?: string, 14 | open?: string, 15 | inactive?: string, 16 | loading?: string, 17 | update?: string, 18 | done?: string, 19 | resolve?: string, 20 | error?: string, 21 | reject?: string, 22 | cancel?: string, 23 | cancelled?: string, 24 | __tagged?: boolean, 25 | toString: Function, 26 | apply: Function, 27 | call: Function 28 | } 29 | 30 | /** 31 | * Actions move through these unique states. 32 | */ 33 | declare type Status = 34 | | 'inactive' 35 | | 'open' 36 | | 'update' 37 | | 'resolve' 38 | | 'reject' 39 | | 'cancel' 40 | -------------------------------------------------------------------------------- /flow/domain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Domains define the rules in which resolved actions are 3 | * converted into new state. They are added to a Microcosm instance 4 | * using `addDomain`. 5 | * @flow 6 | */ 7 | 8 | import type Microcosm from '../src/microcosm' 9 | 10 | declare interface Domain { 11 | /** 12 | * Generate the starting value for the particular state this domain is 13 | * managing. This will be called by the Microcosm using this domain when 14 | * it is started. 15 | */ 16 | getInitialState(): *, 17 | 18 | /** 19 | * Setup runs right after a domain is added to a Microcosm, but 20 | * before it runs getInitialState. This is useful for one-time setup 21 | * instructions. 22 | */ 23 | setup(repo?: Microcosm, options?: Object): void, 24 | 25 | /** 26 | * Runs whenever a Microcosm is torn down. This usually happens when 27 | * a Presenter component unmounts. Useful for cleaning up work done 28 | * in `setup()`. 29 | */ 30 | teardown(repo?: Microcosm): void, 31 | 32 | /** 33 | * Allows a domain to transform data before it leaves the system. It 34 | * gives the domain the opportunity to reduce non-primitive values 35 | * into JSON. 36 | */ 37 | serialize(state: *): *, 38 | 39 | /** 40 | * Allows data to be transformed into a valid shape before it enters a 41 | * Microcosm. This is the reverse operation to `serialize`. 42 | */ 43 | deserialize(data: *): *, 44 | 45 | /** 46 | * Returns an object mapping actions to methods on the domain. This is the 47 | * communication point between a domain and the rest of the system. 48 | */ 49 | register(): ?{ 50 | [key: string | Function]: (last?: *, next?: *) => * 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /flow/effect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import type Microcosm from '../src/microcosm' 6 | import type Action from '../src/action' 7 | 8 | declare interface Effect { 9 | /** 10 | * Setup runs right after an effect is added to a Microcosm. It 11 | * receives that repo and any options passed as the second argument. 12 | */ 13 | setup(repo: Microcosm, options: ?Object): void, 14 | 15 | /** 16 | * Runs whenever a Microcosm is torn down. This usually happens when 17 | * a Presenter component unmounts. Useful for cleaning up work done 18 | * in `setup()`. 19 | */ 20 | teardown(repo: Microcosm): void, 21 | 22 | /** 23 | * Returns an object mapping actions to methods on the effect. This is the 24 | * communication point between a effect and the rest of the system. 25 | */ 26 | register(): { 27 | [any]: (repo: Microcosm, action: Action) => void 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /flow/events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | declare type Callback = (...args: *[]) => void 6 | -------------------------------------------------------------------------------- /flow/form-serialize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | declare module 'form-serialize' { 6 | declare module.exports: (form: Object, options?: Object) => Object 7 | } 8 | -------------------------------------------------------------------------------- /flow/presenter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | declare type Sender = (intent: Command | Tagged, ...params: *[]) => * 6 | -------------------------------------------------------------------------------- /flow/registry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import type Microcosm from '../src/microcosm' 6 | import { type KeyPath } from '../src/key-path' 7 | 8 | declare type Handler = (last?: *, next?: *) => * 9 | 10 | declare type Registration = { 11 | key: KeyPath, 12 | steps: Handler[], 13 | scope: any 14 | } 15 | 16 | declare type Registrations = Registration[] 17 | -------------------------------------------------------------------------------- /flow/snapshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A snapshot is a reference to a specific dispatch for 3 | * a given action. A microcosm instance maintains a bank of them to 4 | * efficiently update state. 5 | * @flow 6 | */ 7 | 8 | type State = { [key: string]: * } 9 | 10 | declare type Snapshot = { 11 | // The last state the snapshot was dispatched with. This is used 12 | // to memoize domain handlers when possible. 13 | last: State, 14 | // The next state for the snapshot. The outcome of `updateSnapshot`. 15 | next: State, 16 | // Last known status of the action associated with this snapshot. 17 | status: Status, 18 | // Last known payload of the action associated with this snapshot. 19 | payload: any, 20 | // Is the current action disabled? This is important triggering a 21 | // state refresh. Enabled actions should always recalculate 22 | disabled: boolean 23 | } 24 | -------------------------------------------------------------------------------- /flow/uid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | declare module 'uid' { 6 | declare module.exports: () => string 7 | } 8 | -------------------------------------------------------------------------------- /jsdoc/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": [], 4 | "dictionaries": ["jsdoc", "closure"] 5 | }, 6 | "plugins": [ 7 | "plugins/markdown", 8 | "node_modules/jsdoc-babel" 9 | ], 10 | "babel": { 11 | "presets": [ 12 | "stage-2", 13 | "react" 14 | ], 15 | "babelrc": false 16 | }, 17 | "templates": { 18 | "cleverLinks": false, 19 | "monospaceLinks": false 20 | }, 21 | "opts": { 22 | "destination": "./new_site/", 23 | "template": "jsdoc/templates/microcosm", 24 | "tutorials": "jsdoc/tutorials", 25 | "recurse": true, 26 | "readme": "./jsdoc/README.md" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /jsdoc/scripts/serve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | var bs = require('browser-sync').create() 6 | // var exec = require('child_process').exec 7 | 8 | // .init starts the server 9 | bs.init({ 10 | port: process.env.PORT || 4000, 11 | open: false, 12 | server: '../../new_site' 13 | }) 14 | 15 | // function make() { 16 | // exec('make -sj', function(error, stdout, stderr) { 17 | // if (error) { 18 | // console.error(`exec error: ${error}`) 19 | // return 20 | // } 21 | // 22 | // if (stdout) { 23 | // console.log(stdout) 24 | // } 25 | // 26 | // if (stderr) { 27 | // console.error(stdout) 28 | // } 29 | // 30 | // bs.reload() 31 | // }) 32 | // } 33 | 34 | // bs.watch('../**/*.md').on('change', make) 35 | // bs.watch('../templates/**/*').on('change', make) 36 | // bs.watch('../tutorials/**/*').on('change', make) 37 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/README.md: -------------------------------------------------------------------------------- 1 | The default template for JSDoc 3 uses: [the Taffy Database library](http://taffydb.com/) and the [Underscore Template library](http://underscorejs.org/). 2 | 3 | 4 | ## Generating Typeface Fonts 5 | 6 | The default template uses the [OpenSans](https://www.google.com/fonts/specimen/Open+Sans) typeface. The font files can be regenerated as follows: 7 | 8 | 1. Open the [OpenSans page at Font Squirrel](). 9 | 2. Click on the 'Webfont Kit' tab. 10 | 3. Either leave the subset drop-down as 'Western Latin (Default)', or, if we decide we need more glyphs, than change it to 'No Subsetting'. 11 | 4. Click the 'DOWNLOAD @FONT-FACE KIT' button. 12 | 5. For each typeface variant we plan to use, copy the 'eot', 'svg' and 'woff' files into the 'templates/default/static/fonts' directory. 13 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/assets/chat-debugger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/microcosm/c5a352cc432fece5c526e25c604363570b2418ee/jsdoc/templates/microcosm/static/assets/chat-debugger.gif -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/assets/microcosm-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/microcosm/c5a352cc432fece5c526e25c604363570b2418ee/jsdoc/templates/microcosm/static/assets/microcosm-badge.png -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/assets/microcosm-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 16 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/assets/microcosm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/microcosm/c5a352cc432fece5c526e25c604363570b2418ee/jsdoc/templates/microcosm/static/assets/microcosm.png -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/static/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/augments.tmpl: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
      8 |
    • 9 |
    10 | 11 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/example.tmpl: -------------------------------------------------------------------------------- 1 | 2 |
    3 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/examples.tmpl: -------------------------------------------------------------------------------- 1 | 8 |

    9 | 10 |
    11 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/exceptions.tmpl: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 |
    7 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 | Type 16 |
    17 |
    18 | 19 |
    20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 | 27 | 28 | 29 | 30 | 31 |
    32 | 33 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/mainpage.tmpl: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 |
    12 |
    13 |
    14 | 15 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/members.tmpl: -------------------------------------------------------------------------------- 1 | 5 |

    6 | 7 | 8 |

    9 | 10 | 11 | 12 |
    13 | 14 |
    15 | 16 | 17 | 18 |
    Type:
    19 |
      20 |
    • 21 | 22 |
    • 23 |
    24 | 25 | 26 | 27 | 28 | 29 |
    Fires:
    30 |
      31 |
    • 32 |
    33 | 34 | 35 | 36 |
    Example 1? 's':'' ?>
    37 | 38 | 39 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/returns.tmpl: -------------------------------------------------------------------------------- 1 | 5 |
    6 | 7 |
    8 | 9 | 10 | 11 |
    12 |
    13 | Type 14 |
    15 |
    16 | 17 |
    18 |
    19 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/source.tmpl: -------------------------------------------------------------------------------- 1 | 4 |
    5 |
    6 |
    7 |
    8 |
    -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/tutorial.tmpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |

    5 | 0) { ?> 6 |
      7 | 10 |
    • 11 | 12 |
    13 | 14 |
    15 | 16 |
    17 | 18 |
    19 | 20 |
    21 | -------------------------------------------------------------------------------- /jsdoc/templates/microcosm/tmpl/type.tmpl: -------------------------------------------------------------------------------- 1 | 5 | 6 | | 7 | -------------------------------------------------------------------------------- /jsdoc/tutorials/recipes.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/microcosm/c5a352cc432fece5c526e25c604363570b2418ee/jsdoc/tutorials/recipes.md -------------------------------------------------------------------------------- /jsdoc/tutorials/recipes/preact.md: -------------------------------------------------------------------------------- 1 | # Preact 2 | 3 | Though the `addons` modules that bundle with Microcosm are designed for React, 4 | we also maintain a `microcosm-preact` project for use with [Preact](https://github.com/developit/preact). 5 | 6 | ## Usage 7 | 8 | Install `microcosm-preact` from npm. 9 | 10 | ``` 11 | npm install --save microcosm-preact 12 | ``` 13 | 14 | From there, it can be used exactly like the standard addons: 15 | 16 | ```javascript 17 | import Presenter from 'microcosm-preact/presenter' 18 | 19 | class MyPresenter extends Presenter { 20 | view () { 21 | return

    All set

    22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /jsdoc/tutorials/tutorials.json: -------------------------------------------------------------------------------- 1 | { 2 | "quickstart": { 3 | "title": "Quickstart", 4 | "children": {} 5 | }, 6 | "recipes": { 7 | "title": "Recipes", 8 | "children": { 9 | "preact": { 10 | "title": "Preact" 11 | }, 12 | "ajax": { 13 | "title": "Ajax" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /new_site/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | public/**/*.html 2 | tmp/* 3 | -------------------------------------------------------------------------------- /site/Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += "-j 4" 2 | 3 | DOCS := $(patsubst ../docs/%.md, public/%.html, $(shell find ../docs -name "*.md")) 4 | PAGES := $(patsubst src/pages/%.md, public/%.html, $(shell find src/pages -name "*.md")) 5 | 6 | all: $(DOCS) $(PAGES) public/changelog.html 7 | 8 | server: clean 9 | @ $(MAKE) all 10 | @ ./scripts/serve 11 | 12 | publish: clean 13 | @ ./scripts/publish 14 | 15 | clean: 16 | @ rm -rf $(PAGES) $(DOCS) 17 | 18 | public/changelog.html: ../CHANGELOG.md src/layouts/default.html 19 | @ ./scripts/build-page $< $@ default 20 | @ echo [+] $@ 21 | 22 | public/index.html: src/pages/index.md src/layouts/home.html 23 | @ ./scripts/build-page $< $@ home 24 | @ echo [+] $@ 25 | 26 | public/%.html: ../docs/%.md src/layouts/default.html 27 | @ mkdir -p $(@D) 28 | @ ./scripts/build-page $< $@ default 29 | @ echo [+] $@ 30 | 31 | .PHONY: all server publish clean 32 | -------------------------------------------------------------------------------- /site/public/assets/chat-debugger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/microcosm/c5a352cc432fece5c526e25c604363570b2418ee/site/public/assets/chat-debugger.gif -------------------------------------------------------------------------------- /site/public/assets/microcosm-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/microcosm/c5a352cc432fece5c526e25c604363570b2418ee/site/public/assets/microcosm-badge.png -------------------------------------------------------------------------------- /site/public/assets/microcosm-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 16 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /site/public/assets/microcosm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/microcosm/c5a352cc432fece5c526e25c604363570b2418ee/site/public/assets/microcosm.png -------------------------------------------------------------------------------- /site/scripts/build-page: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OPTIONS="-s --smart --from=markdown-implicit_figures --var=base_url:$BASE_URL" 4 | TEMPLATE=src/layouts/$3.html 5 | 6 | pandoc $OPTIONS --template=$TEMPLATE $1 | sed "s:\\.md:\\.html:g" > $2 7 | -------------------------------------------------------------------------------- /site/scripts/publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export BASE_URL=$(node -p 'require("../package").homepage') 4 | 5 | make all 6 | 7 | # Clean up pesky files 8 | rm -rf ./**/.DS_Store 9 | 10 | cd .. 11 | git checkout -B gh-pages 12 | git add -f site/public 13 | git commit -am "[skip ci] Rebuild website" 14 | git filter-branch -f --prune-empty --subdirectory-filter site/public 15 | git push -f origin gh-pages 16 | git checkout - 17 | cd site 18 | 19 | echo "" 20 | echo "Published to:" 21 | echo "$BASE_URL" 22 | echo "" 23 | -------------------------------------------------------------------------------- /site/scripts/serve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | var bs = require("browser-sync").create() 6 | var exec = require('child_process').exec 7 | 8 | // .init starts the server 9 | bs.init({ 10 | port : process.env.PORT || 4000, 11 | open : false, 12 | server : "./public" 13 | }) 14 | 15 | function make () { 16 | exec('make -sj', function (error, stdout, stderr) { 17 | if (error) { 18 | console.error(`exec error: ${error}`) 19 | return 20 | } 21 | 22 | if (stdout) { 23 | console.log(stdout) 24 | } 25 | 26 | if (stderr) { 27 | console.error(stdout) 28 | } 29 | 30 | bs.reload() 31 | }) 32 | } 33 | 34 | 35 | bs.watch('../**/*.md').on('change', make) 36 | bs.watch('./src/**/*').on('change', make) 37 | -------------------------------------------------------------------------------- /site/src/layouts/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Microcosm 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 |
    21 | 22 | Microcosm 23 | 24 | 25 | 30 |
    31 |
    32 | 33 |
    34 |
    35 |
    36 |

    An evolution of Flux

    37 |

    38 | Microcosm is 39 | Flux 40 | with actions at center stage. Write optimistic updates, 41 | cancel requests, and track changes with ease. 42 |

    43 | 48 |
    49 |
    50 | 51 |
    $body$
    52 |
    53 | 54 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/addons/frame.js: -------------------------------------------------------------------------------- 1 | // Cross platform request animation frame 2 | 3 | const hasFrame = typeof requestAnimationFrame !== 'undefined' 4 | 5 | let requestFrame = setTimeout 6 | let cancelFrame = clearTimeout 7 | 8 | /* istanbul ignore if */ 9 | if (hasFrame) { 10 | requestFrame = requestAnimationFrame 11 | cancelFrame = cancelAnimationFrame 12 | } 13 | 14 | export { requestFrame, cancelFrame } 15 | -------------------------------------------------------------------------------- /src/addons/jest-matchers.js: -------------------------------------------------------------------------------- 1 | import Microcosm, { Action, tag, get, getRegistration } from 'microcosm' 2 | 3 | expect.extend({ 4 | toRegister(entity, command, status = 'done') { 5 | let tagged = tag(command) 6 | let name = command.name || tagged.toString() 7 | 8 | let operator = this.isNot ? 'not to' : 'to' 9 | let pass = false 10 | 11 | if (entity.register) { 12 | pass = getRegistration(entity.register(), command, status) != null 13 | } else { 14 | throw new TypeError(`${entity.constructor.name} has no register method`) 15 | } 16 | 17 | return { 18 | pass: pass, 19 | message: () => { 20 | return `Expected entity ${operator} register to the '${status}' state of ${name}.` 21 | } 22 | } 23 | }, 24 | 25 | toHaveStatus(action, status) { 26 | if (action instanceof Action === false) { 27 | throw new TypeError( 28 | 'toHaveStatus expects an Action. Received ' + 29 | (action != null ? 'a ' + action.constructor.name : action) + 30 | '.' 31 | ) 32 | } 33 | 34 | let operator = this.isNot ? 'not to' : 'to' 35 | let pass = action.is(status) 36 | 37 | return { 38 | pass: pass, 39 | message: () => { 40 | return `Expected action ${operator} to be'${status}'.` 41 | } 42 | } 43 | }, 44 | 45 | toHaveState(repo, key, value) { 46 | if (repo instanceof Microcosm === false) { 47 | throw new TypeError( 48 | 'toHaveState expects a Microcosm. Received ' + 49 | (repo != null ? 'a ' + repo.constructor.name : repo) + 50 | '.' 51 | ) 52 | } 53 | 54 | let operator = this.isNot ? 'not to' : 'to' 55 | let pass = false 56 | let actual = get(repo.state, key) 57 | 58 | if (arguments.length > 2) { 59 | pass = JSON.stringify(actual) === JSON.stringify(value) 60 | } else { 61 | pass = actual !== undefined 62 | } 63 | 64 | // Display friendly key path 65 | let path = [].concat(key).join('.') 66 | 67 | return { 68 | pass: pass, 69 | message: () => { 70 | return ( 71 | `Expected '${path}' in repo.state ${operator} be ${this.utils.printExpected( 72 | value 73 | )} ` + `but it is ${this.utils.printReceived(actual)}.` 74 | ) 75 | } 76 | } 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /src/addons/with-send.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Connect a component to the presenter tree 3 | * @flow 4 | */ 5 | 6 | import { createElement } from 'react' 7 | import { merge } from 'microcosm' 8 | 9 | function displayName(Component: Object) { 10 | return Component.displayName || Component.name || 'Component' 11 | } 12 | 13 | const CONTEXT_TYPES = { 14 | send: () => {} 15 | } 16 | 17 | export default function withSend(Component: *): * { 18 | function withSend(props: Object, context: Object) { 19 | let send = props.send || context.send 20 | 21 | return createElement(Component, merge({ send }, props)) 22 | } 23 | 24 | withSend.displayName = 'withSend(' + displayName(Component) + ')' 25 | 26 | withSend.contextTypes = CONTEXT_TYPES 27 | 28 | withSend.WrappedComponent = Component 29 | 30 | return withSend 31 | } 32 | -------------------------------------------------------------------------------- /src/compare-tree/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A node in a CompareTree represents a single key with 3 | * a JavaScript object. 4 | * @flow 5 | */ 6 | 7 | import assert from 'assert' 8 | import type Query from './query' 9 | 10 | class Node { 11 | id: string 12 | key: string 13 | parent: ?Node 14 | edges: Array 15 | 16 | static getId(key, parent) { 17 | return parent && parent.id ? parent.id + '.' + key : key 18 | } 19 | 20 | constructor(id: string, key: string, parent: ?Node) { 21 | this.id = id 22 | this.key = key 23 | this.edges = [] 24 | this.parent = parent || null 25 | 26 | if (parent) { 27 | parent.connect(this) 28 | } 29 | } 30 | 31 | /** 32 | * Connect another node to this instance by adding it to 33 | * the list of edges. 34 | */ 35 | connect(node: Node | Query) { 36 | assert( 37 | this.edges.indexOf(node) <= 0, 38 | node.id + ' is already connected to ' + this.id 39 | ) 40 | 41 | assert(node !== this, 'Unable to connect node ' + node.id + ' to self.') 42 | 43 | this.edges.push(node) 44 | } 45 | 46 | /** 47 | * Remove a node this instances list of edges. 48 | */ 49 | disconnect(node: Node | Query) { 50 | let index = this.edges.indexOf(node) 51 | 52 | if (~index) { 53 | this.edges.splice(index, 1) 54 | } 55 | } 56 | 57 | /** 58 | * Does a node have any edges? 59 | */ 60 | isAlone(): boolean { 61 | return this.edges.length <= 0 62 | } 63 | 64 | /** 65 | * Disconnect from a parent 66 | */ 67 | orphan() { 68 | if (this.parent) { 69 | this.parent.disconnect(this) 70 | } 71 | } 72 | } 73 | 74 | export default Node 75 | -------------------------------------------------------------------------------- /src/compare-tree/query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Leaf nodes of the comparison tree are queries. A 3 | * grouping of data subscriptions. 4 | * @flow 5 | */ 6 | 7 | import Emitter from '../emitter' 8 | import { get } from '../utils' 9 | import { getKeyStrings, type KeyPath } from '../key-path' 10 | 11 | class Query extends Emitter { 12 | id: string 13 | keyPaths: KeyPath[] 14 | 15 | static getId(keyPaths: KeyPath[]) { 16 | return 'query:' + getKeyStrings(keyPaths) 17 | } 18 | 19 | constructor(id: string, keys: KeyPath[]) { 20 | super() 21 | 22 | this.id = id 23 | this.keyPaths = keys 24 | } 25 | 26 | trigger(state: Object) { 27 | let args = ['change'] 28 | 29 | for (var i = 0, len = this.keyPaths.length; i < len; i++) { 30 | args[i + 1] = get(state, this.keyPaths[i]) 31 | } 32 | 33 | this._emit(...args) 34 | } 35 | 36 | isAlone() { 37 | return this._events.length <= 0 38 | } 39 | } 40 | 41 | export default Query 42 | -------------------------------------------------------------------------------- /src/default-update-strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is the default update strategy for Microcosm. It 3 | * can be overriden by passing the `updater` option when creating a 4 | * Microcosm. 5 | * @flow 6 | */ 7 | 8 | /** 9 | * requestIdleCallback isn't supported everywhere and is hard to 10 | * polyfill. For environments that do not support it, just use a 11 | * setTimeout. 12 | * 13 | * Note: To be fully compliant, we would invoke the callback with the 14 | * time remaining. Given our usage, we don't need to do that. 15 | * @private 16 | */ 17 | export type Updater = (update: Function, options: Object) => void | * 18 | 19 | type UpdateOptions = { 20 | batch: boolean 21 | } 22 | 23 | const scheduler: Updater = 24 | typeof requestIdleCallback !== 'undefined' 25 | ? requestIdleCallback 26 | : update => setTimeout(update, 4) 27 | 28 | /** 29 | * When using requestIdleCallback, batch together updates until the 30 | * browser is ready for them, but never make the user wait longer than 31 | * 36 milliseconds. 32 | * @private 33 | */ 34 | const BATCH_OPTIONS = { timeout: 36 } 35 | 36 | export default function defaultUpdateStrategy(options: UpdateOptions): Updater { 37 | return update => { 38 | if (options.batch === true) { 39 | scheduler(update, BATCH_OPTIONS) 40 | } else { 41 | update() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/effect-engine.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import getRegistration from './get-registration' 6 | import { merge, createOrClone } from './utils' 7 | import assert from 'assert' 8 | import type Action from './action' 9 | import type Microcosm from './microcosm' 10 | 11 | class EffectEngine { 12 | repo: Microcosm 13 | effects: Array 14 | 15 | constructor(repo: Microcosm) { 16 | this.repo = repo 17 | this.effects = [] 18 | } 19 | 20 | add(config: Object | Function, options?: Object) { 21 | assert( 22 | !options || options.constructor === Object, 23 | 'addEffect expected a plain object as the second argument.' 24 | ) 25 | 26 | let deepOptions = merge(this.repo.options, config.defaults, options) 27 | let effect: Effect = createOrClone(config, deepOptions, this.repo) 28 | 29 | if (effect.setup) { 30 | effect.setup(this.repo, deepOptions) 31 | } 32 | 33 | if (effect.teardown) { 34 | this.repo.on('teardown', effect.teardown, effect) 35 | } 36 | 37 | this.effects.push(effect) 38 | 39 | return effect 40 | } 41 | 42 | dispatch(action: Action) { 43 | let { command, payload, status } = action 44 | 45 | for (var i = 0; i < this.effects.length; i++) { 46 | var effect = this.effects[i] 47 | 48 | if (effect.register) { 49 | let handlers = getRegistration(effect.register(), command, status) 50 | 51 | for (var j = 0; j < handlers.length; j++) { 52 | handlers[j].call(effect, this.repo, payload) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | export default EffectEngine 60 | -------------------------------------------------------------------------------- /src/get-registration.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isObject } from './utils' 3 | import assert from 'assert' 4 | 5 | export const STATUSES = { 6 | inactive: 'inactive', 7 | open: 'open', 8 | update: 'loading', 9 | loading: 'update', 10 | done: 'resolve', 11 | resolve: 'done', 12 | reject: 'error', 13 | error: 'reject', 14 | cancel: 'cancelled', 15 | cancelled: 'cancel' 16 | } 17 | 18 | function isPlainObject(value) { 19 | return !Array.isArray(value) && isObject(value) 20 | } 21 | 22 | /** 23 | * Gets any registrations that match a given command and status. 24 | */ 25 | function getRegistration(pool: ?Object, command: Tagged, status: Status) { 26 | let answer = [] 27 | 28 | if (pool == null) { 29 | return answer 30 | } 31 | 32 | let alias = STATUSES[status] 33 | 34 | assert(alias, 'Invalid action status ' + status) 35 | assert( 36 | command.__tagged, 37 | `Unable to register ${command.name || 'action'}(). It has not been tagged.` 38 | ) 39 | 40 | let nest = pool[command] 41 | let type = command[status] || '' 42 | 43 | /** 44 | * Support nesting, like: 45 | */ 46 | if (isPlainObject(nest)) { 47 | answer = nest[alias] || nest[status] 48 | 49 | /** 50 | * Throw in strict mode if a nested registration is undefined. This is usually a typo. 51 | */ 52 | assert( 53 | !(alias in nest) || answer !== undefined, 54 | `The "${alias}" key within a nested registration for ${command.name || 55 | 'an action'} is ${answer}. Is it being referenced correctly?` 56 | ) 57 | assert( 58 | !(status in nest) || answer !== undefined, 59 | `The "${status}" key within a nested registration for ${command.name || 60 | 'an action'} is ${answer}. Is it being referenced correctly?` 61 | ) 62 | } else { 63 | answer = pool[type] 64 | 65 | /** 66 | * Similarly, throw in strict mode if a regular registration is undefined. This is usually a typo. 67 | */ 68 | assert( 69 | !(type in pool) || answer !== undefined, 70 | `${command.name || 71 | 'action'} key within a registration is ${answer}. Is it being referenced correctly?` 72 | ) 73 | } 74 | 75 | if (answer) { 76 | return Array.isArray(answer) ? answer : [answer] 77 | } 78 | 79 | return [] 80 | } 81 | 82 | export default getRegistration 83 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { Microcosm, Microcosm as default } from './microcosm' 2 | 3 | export { get, set, update, merge } from './utils' 4 | 5 | export { default as Action } from './action' 6 | 7 | export { default as History } from './history' 8 | 9 | export { default as Emitter } from './emitter' 10 | 11 | export { default as tag } from './tag' 12 | 13 | export { default as getRegistration } from './get-registration' 14 | -------------------------------------------------------------------------------- /src/install-devtools.js: -------------------------------------------------------------------------------- 1 | const key = '__MICROCOSM_DEVTOOLS_GLOBAL_HOOK__' 2 | 3 | export default function installDevtools(repo) { 4 | let namespace = typeof global === 'undefined' ? window : global 5 | let hook = namespace[key] 6 | 7 | if (hook) { 8 | hook.emit('init', repo) 9 | 10 | repo.history.setLimit(Infinity) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/key-path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A key path is a list of property names that describe 3 | * a pathway through a nested javascript object. For example, 4 | * `['users', 2]` could represent a path within in `{ users: [{id: 0}, 5 | * {id: 1}] }` 6 | * @flow 7 | */ 8 | 9 | export type KeyPath = Array 10 | 11 | const KEY_DELIMETER = '.' 12 | const PATH_DELIMETER = ',' 13 | 14 | function isBlank(value: any): boolean { 15 | return value === '' || value === null || value === undefined 16 | } 17 | 18 | /** 19 | * Ensure a value is a valid key path. 20 | * @private 21 | */ 22 | export function castPath(value: string | KeyPath): KeyPath { 23 | if (Array.isArray(value)) { 24 | return value 25 | } else if (isBlank(value)) { 26 | return [] 27 | } 28 | 29 | return typeof value === 'string' ? value.trim().split(KEY_DELIMETER) : [value] 30 | } 31 | 32 | /** 33 | * Convert a value into a list of key paths. These paths may be comma 34 | * separated, which is used in the CompareTree to describe a 35 | * subscription to multiple pathways in an object. 36 | * @private 37 | */ 38 | export function getKeyPaths(value: string | Array): Array { 39 | if (typeof value === 'string') { 40 | return `${value}`.split(PATH_DELIMETER).map(castPath) 41 | } 42 | 43 | return value.every(Array.isArray) ? value : value.map(castPath) 44 | } 45 | 46 | /** 47 | * Convert a key path into a string. 48 | * @private 49 | */ 50 | export function getKeyString(value: KeyPath): string { 51 | return value.join(KEY_DELIMETER) 52 | } 53 | 54 | /** 55 | * Convert a list of keys path into a string. 56 | * @private 57 | */ 58 | export function getKeyStrings(array: Array): string { 59 | return array.map(getKeyString).join(PATH_DELIMETER) 60 | } 61 | -------------------------------------------------------------------------------- /src/lifecycle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import assert from 'assert' 6 | import { mergeSame } from './utils' 7 | import type Action from './action' 8 | import type Microcosm from './microcosm' 9 | 10 | function sandbox(data: Object, deserialize: boolean) { 11 | return (action: Action, repo: Microcosm): void => { 12 | let payload = data 13 | 14 | if (deserialize) { 15 | try { 16 | payload = repo.deserialize(data) 17 | } catch (error) { 18 | action.reject(error) 19 | throw error 20 | } 21 | } 22 | // Strip out keys not managed by this repo. This prevents children from 23 | // accidentally having their keys reset by parents. 24 | action.resolve(mergeSame(repo.state, payload)) 25 | } 26 | } 27 | 28 | export const RESET = function $reset(data: Object, deserialize: boolean) { 29 | return sandbox(data, deserialize) 30 | } 31 | 32 | export const PATCH = function $patch(data: Object, deserialize: boolean) { 33 | return sandbox(data, deserialize) 34 | } 35 | 36 | export const BIRTH = function $birth() { 37 | assert(false, 'Birth lifecycle method should never be invoked directly.') 38 | } 39 | 40 | export const START = function $start() { 41 | assert(false, 'Start lifecycle method should never be invoked directly.') 42 | } 43 | 44 | export const ADD_DOMAIN = function $addDomain(domain: Domain) { 45 | return domain 46 | } 47 | -------------------------------------------------------------------------------- /src/meta-domain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Every Microcosm includes MetaDomain. It provides the 3 | * plumbing required for lifecycle methods like `reset` and `patch`. 4 | * @flow 5 | */ 6 | 7 | import { mergeSame, merge } from './utils' 8 | import { RESET, PATCH, ADD_DOMAIN } from './lifecycle' 9 | 10 | class MetaDomain { 11 | repo: Microcosm 12 | 13 | setup(repo: Microcosm) { 14 | this.repo = repo 15 | } 16 | 17 | /** 18 | * Build a new Microcosm state object. 19 | */ 20 | reset(oldState: Object, newState: Object): Object { 21 | let filtered = mergeSame(oldState, newState) 22 | 23 | return merge(oldState, this.repo.getInitialState(), filtered) 24 | } 25 | 26 | /** 27 | * Merge a state object into the current Microcosm state. 28 | */ 29 | patch(oldState: Object, newState: Object): Object { 30 | let filtered = mergeSame(oldState, newState) 31 | 32 | return merge(oldState, filtered) 33 | } 34 | 35 | /** 36 | * Update the initial state whenever a new domain is added to a 37 | * repo. 38 | */ 39 | addDomain(oldState: Object): Object { 40 | return merge(this.repo.getInitialState(), oldState) 41 | } 42 | 43 | register() { 44 | // TODO: Flow does not like string coercion. How can we 45 | // get Flow type coverage on the register method? 46 | var registry = { 47 | [RESET.toString()]: this.reset, 48 | [PATCH.toString()]: this.patch, 49 | [ADD_DOMAIN.toString()]: this.addDomain 50 | } 51 | 52 | return registry 53 | } 54 | } 55 | 56 | export default MetaDomain 57 | -------------------------------------------------------------------------------- /src/symbols.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | const $Symbol = typeof Symbol === 'function' ? Symbol : {} 6 | 7 | export const toStringTag: * = $Symbol.toStringTag || '@@toStringTag' 8 | 9 | export const iteratorTag: * = $Symbol.iterator || '@@iterator' 10 | -------------------------------------------------------------------------------- /src/tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | import assert from 'assert' 6 | 7 | let uid = 0 8 | const FALLBACK = '_action' 9 | 10 | /** 11 | * Uniquely tag a function. This is used to identify actions. 12 | */ 13 | export default function tag(fn: Command | Tagged, name?: string): Tagged { 14 | assert( 15 | fn != undefined, 16 | `Unable to identify ${fn == null ? fn : fn.toString()} action.` 17 | ) 18 | 19 | if (typeof fn === 'string') { 20 | return tag(n => n, fn) 21 | } 22 | 23 | if (fn.__tagged === true) { 24 | return fn 25 | } 26 | 27 | /** 28 | * Auto-increment a stepper suffix to prevent two actions with the 29 | * same name from colliding. 30 | */ 31 | uid += 1 32 | 33 | /** 34 | * Function.name lacks legacy support. For these browsers, fallback 35 | * to a consistent name: 36 | */ 37 | const symbol = name || (fn.name || FALLBACK) + '.' + uid 38 | 39 | // Cast fn to keep Flow happy 40 | let cast: Tagged = fn 41 | 42 | cast.open = symbol + '.open' 43 | 44 | cast.loading = symbol + '.loading' 45 | cast.update = cast.loading 46 | 47 | cast.done = symbol // intentional for string actions 48 | cast.resolve = cast.done 49 | 50 | cast.error = symbol + '.error' 51 | cast.reject = cast.error 52 | 53 | cast.cancel = symbol + '.cancel' 54 | cast.cancelled = fn.cancel 55 | 56 | // The default state is done 57 | cast.toString = () => symbol 58 | 59 | // Mark the function as tagged so we only do this once 60 | fn.__tagged = true 61 | 62 | return cast 63 | } 64 | -------------------------------------------------------------------------------- /test/addons/with-send.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import React from 'react' 6 | import withSend from '../../src/addons/with-send' 7 | import { mount } from 'enzyme' 8 | 9 | it('exposes the wrapped component as a static property', function() { 10 | const Button = function({ send }) { 11 | return ( 12 | 15 | ) 16 | } 17 | 18 | const WrappedButton = withSend(Button) 19 | 20 | expect(WrappedButton.WrappedComponent).toEqual(Button) 21 | }) 22 | 23 | it('extracts send from context', function() { 24 | const Button = withSend(function({ send }) { 25 | return ( 26 | 29 | ) 30 | }) 31 | 32 | const send = jest.fn() 33 | 34 | const component = mount( 54 | ) 55 | }) 56 | 57 | mount(