Repository: vigetlabs/microcosm
Pull request #329 (2017-05-23 11:19:40)

Microcosms can be register to actions
Microcosms can now declare a custom register and getInitialState method. In this way, they behave exactly like domains.

This does not allow Microcosms to be added as domains to other Microcosms. That will require some work. This is intended to make it easier to introduce Microcosm, and for concise examples.

```javascript
const ADD = 'ADD'

class Repo extends Microcosm {
  getInitialState() {
    return { count: 0 }
  }

  add(state, n) {
    return update(state, 'count', value => value + n)
  }

  register() {
    return {
      [ADD]: this.add
    }
  }
}

let repo = new Repo()

repo.push(ADD, 2)
repo.push(ADD, 2)

expect(repo).toHaveState('count', 4)
```

---

**What kind of change does this PR introduce?** (check at least one)

- [x] Feature

**Does this PR introduce a breaking change?** (check one)

- [x] No

**Does this PR fulfill these requirements:**

- [x] All tests are passing
- [x] Commits reference open issues

codecov-io: # [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=h1) Report
> Merging [#329](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=desc) into [master](https://codecov.io/gh/vigetlabs/microcosm/commit/7e25c4a287ded3479125ac24100dabf9b185b693?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/vigetlabs/microcosm/pull/329/graphs/tree.svg?width=650&height=150&src=pr&token=AaE0FLz9sU)](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #329   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          24     24           
  Lines         822    829    +7     
=====================================
+ Hits          822    829    +7
```


| [Impacted Files](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/microcosm.js](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=tree#diff-c3JjL21pY3JvY29zbS5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [src/domain-engine.js](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=tree#diff-c3JjL2RvbWFpbi1lbmdpbmUuanM=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=footer). Last update [7e25c4a...0d697a9](https://codecov.io/gh/vigetlabs/microcosm/pull/329?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

efatsi: cool stuff, 2 things I'm curious about:
- would you mind explaining the motivation behind the change to `sanitize()`
  - also, digging into when `sanitize` is invoked, is it possible that it's invoked multiple times? looking at the following execution path:
    - [Microcosm::patch](https://github.com/vigetlabs/microcosm/blob/3de6df6c668054bffd555aa4d8be531ba35758ec/src/microcosm.js#L315-L317)
    - [Lifecycle::PATCH -> sandbox](https://github.com/vigetlabs/microcosm/blob/3de6df6c668054bffd555aa4d8be531ba35758ec/src/lifecycle.js#L18) calls `repo.domains.sanitize`
    - [MetaDomain::patch](https://github.com/vigetlabs/microcosm/blob/3de6df6c668054bffd555aa4d8be531ba35758ec/src/meta-domain.js#L29-L33) is registered to PATCH action and also calls `repo.domains.sanitize`
- What are your thoughts on simply merging `repo.register` within the body of `MetaDomain::register`? So instead of pushing `[[], this.repo]` onto the list of Domains, the action registration functionality is just handled by the MetaDomain. The fact that a Microcosm instance would live side by side with all of it's Domains seems a little funky to me conceptually, and more like a convenient hack to broaden the reach of the DomainEngine's functionality. Also, pushing it on the stack even when it has no `register` functionality seems wasteful.
nhunzaker: @efatsi I like the idea of merging register into the meta domain. The problem is that scope then becomes the meta domain, instead of the Microcosm, so if you call another method, like `this.something`, the method is accessed on MetaDomain, not the microcosm.

As for sanitize. I'll dig in here. Basically, we want to prevent forks from patching new state into their parents. Like if you do:

```
let repo = new Microcosm()
let fork = repo.fork()

fork.patch({ foo: 'bar' })
```

`repo.state` should not include `foo`.
efatsi: @nhunzaker gotchya. well then my only suggestion would be to wrap the `[[], this.repo]` push in it's own method, and possibly only invoke it if you can determine that the repo has implemented it's own register.

otherwise, 👍 

Patch:
diff --git a/src/domain-engine.js b/src/domain-engine.js
index 6eeb77fc..7dd4d0c1 100644
--- a/src/domain-engine.js
+++ b/src/domain-engine.js
@@ -1,19 +1,28 @@
 import MetaDomain from './meta-domain'
 import getRegistration from './get-registration'
 import { get, set, createOrClone } from './utils'
-import { castPath } from './key-path'
+import { castPath, getKeyString } from './key-path'
 
 class DomainEngine {
   /**
    * @param {Microcosm} repo
    */
   constructor(repo) {
+    this.registry = {}
     this.repo = repo
     this.domains = []
-    this.registry = {}
 
     // All realms contain a meta domain for basic Microcosm operations
     this.add([], MetaDomain)
+
+    // Microcosms can register actions
+    this.addRepo(repo)
+  }
+
+  addRepo(repo) {
+    if (typeof repo.register !== 'undefined') {
+      this.domains.push([[], repo])
+    }
   }
 
   getHandlers({ command, status }) {
@@ -76,14 +85,26 @@ class DomainEngine {
     return next
   }
 
+  supportsKey(key) {
+    if (key in this.repo.state) {
+      return true
+    }
+
+    return this.domains.some(entry => getKeyString(entry[0]) === key)
+  }
+
   sanitize(data) {
+    let repo = this.repo
+    let parent = repo.parent
     let next = {}
 
-    for (var i = 0, len = this.domains.length; i < len; i++) {
-      let [key] = this.domains[i]
+    for (var key in data) {
+      if (parent && parent.domains.supportsKey(key)) {
+        continue
+      }
 
-      if (key.length) {
-        next = set(next, key, get(data, key))
+      if (this.supportsKey(key)) {
+        next[key] = data[key]
       }
     }
 
diff --git a/src/microcosm.js b/src/microcosm.js
index c08fea93..68a50e3f 100644
--- a/src/microcosm.js
+++ b/src/microcosm.js
@@ -94,7 +94,7 @@ class Microcosm extends Emitter {
 
     this.parent = options.parent
 
-    this.initial = this.parent ? this.parent.initial : {}
+    this.initial = this.parent ? this.parent.initial : this.getInitialState()
     this.state = this.parent ? this.parent.state : this.initial
 
     this.history = this.parent ? this.parent.history : new History(options)
@@ -173,7 +173,7 @@ class Microcosm extends Emitter {
    * Generates the starting state for a Microcosm instance. This is the result of dispatching `getInitialState` to all domains. It is pure; calling this function will not update state.
    */
   getInitialState() {
-    return this.initial
+    return this.initial == null ? {} : this.initial
   }
 
   recall(action, fallback) {


Test patch:
diff --git a/test/addons/presenter.test.js b/test/addons/presenter.test.js
index 4bbc8c8d..4d7ebbae 100644
--- a/test/addons/presenter.test.js
+++ b/test/addons/presenter.test.js
@@ -9,12 +9,8 @@ const View = withSend(function({ send }) {
 })
 
 class Repo extends Microcosm {
-  setup() {
-    this.addDomain('color', {
-      getInitialState() {
-        return 'yellow'
-      }
-    })
+  getInitialState() {
+    return { color: 'yellow' }
   }
 }
 
diff --git a/test/unit/microcosm/patch.test.js b/test/unit/microcosm/patch.test.js
index 4ada8624..e05e99f1 100644
--- a/test/unit/microcosm/patch.test.js
+++ b/test/unit/microcosm/patch.test.js
@@ -11,6 +11,20 @@ describe('Microcosm::patch', function() {
     expect(repo).not.toHaveState('shapes')
   })
 
+  it('allows patching state for domains that operate on the root', function() {
+    const repo = new Microcosm()
+
+    repo.addDomain('', {
+      getInitialState() {
+        return { shapes: [] }
+      }
+    })
+
+    repo.patch({ shapes: ['square', 'triangle', 'circle'] })
+
+    expect(repo).toHaveState('shapes', ['square', 'triangle', 'circle'])
+  })
+
   it('raises if there is a JSON parse error deserialization fails', function() {
     const repo = new Microcosm()
 
diff --git a/test/unit/microcosm/register.test.js b/test/unit/microcosm/register.test.js
new file mode 100644
index 00000000..d8ed2a68
--- /dev/null
+++ b/test/unit/microcosm/register.test.js
@@ -0,0 +1,30 @@
+import Microcosm, { update } from '../../../src/microcosm'
+
+describe('Microcosm::register', function() {
+  it('a Microcosm can register to actiosn', function() {
+    const ADD = 'ADD'
+
+    class Repo extends Microcosm {
+      getInitialState() {
+        return { count: 0 }
+      }
+
+      add(state, n) {
+        return update(state, 'count', value => value + n)
+      }
+
+      register() {
+        return {
+          [ADD]: this.add
+        }
+      }
+    }
+
+    let repo = new Repo()
+
+    repo.push(ADD, 2)
+    repo.push(ADD, 2)
+
+    expect(repo).toHaveState('count', 4)
+  })
+})