Repository: vigetlabs/microcosm
Pull request #435 (2017-10-12 14:11:26)

Repos and Forks can not add multiple domains for the same key
This commit removes the ability to add multiple domains for the same key. This allows us to:

1. Remove the meta-domain
2. Remove the ADD_DOMAIN lifecycle action
3. Avoid merging data to avoid "spilling" keys

**Breaking changes**

1. Microcosms can no longer implement `register()`. This was intended to make it easier to build a Microcosm, but it is never used in real-world apps™️ and is problematic for future work.
2. Forks can not add domains on top of parents
3. Microcosms can not add multiple domains for the same key

Fixes https://github.com/vigetlabs/microcosm/issues/427.


Patch:
diff --git a/packages/microcosm/flow/registry.js.flow b/packages/microcosm/flow/registry.js.flow
index 4d262606..3db6e332 100644
--- a/packages/microcosm/flow/registry.js.flow
+++ b/packages/microcosm/flow/registry.js.flow
@@ -2,15 +2,15 @@
  * @flow
  */
 
-import type Microcosm from '../src/microcosm'
-import { type KeyPath } from '../src/key-path'
-
-declare type Handler = (last?: *, next?: *) => *
+declare type Handler = (last?: mixed, next?: any) => *
 
 declare type Registration = {
-  key: KeyPath,
+  key: string,
   steps: Handler[],
-  scope: any
+  scope: any,
+  local: boolean
 }
 
 declare type Registrations = Registration[]
+
+declare type Registry = { [action: string]: Registrations }
diff --git a/packages/microcosm/src/action.js b/packages/microcosm/src/action.js
index 88d1e128..f3cb7f99 100644
--- a/packages/microcosm/src/action.js
+++ b/packages/microcosm/src/action.js
@@ -37,8 +37,7 @@ class Action extends Emitter {
   timestamp: number
   children: Action[]
   revisions: Revision[]
-
-  _origin: Microcosm
+  origin: Microcosm
 
   constructor(command: string | Command, status: ?Status, origin: Microcosm) {
     super()
@@ -54,8 +53,7 @@ class Action extends Emitter {
     this.timestamp = Date.now()
     this.children = []
     this.revisions = []
-
-    this._origin = origin
+    this.origin = origin
 
     if (status) {
       this._setState(status)
@@ -86,8 +84,8 @@ class Action extends Emitter {
     return this._setState.bind(this, 'cancel')
   }
 
-  execute(...params: *[]): this {
-    coroutine(this, params, this._origin)
+  execute(params: *[]): this {
+    coroutine(this, params, this.origin)
     return this
   }
 
@@ -307,7 +305,7 @@ class Action extends Emitter {
   }
 
   _setState(status: Status, payload: mixed) {
-    if (this.complete) {
+    if (this.complete === true) {
       return this
     }
 
diff --git a/packages/microcosm/src/addons/indexing.js b/packages/microcosm/src/addons/indexing.js
index f8768071..39c94ec5 100644
--- a/packages/microcosm/src/addons/indexing.js
+++ b/packages/microcosm/src/addons/indexing.js
@@ -50,13 +50,13 @@ export default function() {
       }, answer)
     }
 
-    this.indexes = set(this.indexes || {}, name, query)
+    this.indexes = set(this.indexes, name, query)
 
     return query
   }
 
   Microcosm.prototype.lookup = function(name) {
-    let index = get(this.indexes, name)
+    let index = get(this.indexes, name, null)
 
     if (index == null) {
       if (this.parent) {
diff --git a/packages/microcosm/src/domain-engine.js b/packages/microcosm/src/domain-engine.js
index 6a3b71c9..fe5edb75 100644
--- a/packages/microcosm/src/domain-engine.js
+++ b/packages/microcosm/src/domain-engine.js
@@ -2,16 +2,14 @@
  * @flow
  */
 
-import MetaDomain from './meta-domain'
 import getRegistration from './get-registration'
-import { get, set, merge, result, createOrClone } from './utils'
-import { castPath, type KeyPath } from './key-path'
+import { clone, merge, result, createOrClone } from './utils'
+import { RESET, PATCH } from './lifecycle'
 
 import type Action from './action'
 import type Microcosm from './microcosm'
 
-type DomainList = Array<[KeyPath, Domain]>
-type Registry = { [action: string]: Registrations }
+type DomainList = { [key: string]: Domain }
 
 /**
  * Reduce down a list of values.
@@ -28,38 +26,33 @@ function reduce(steps: Handler[], payload: *, start: *, scope: any) {
 
 class DomainEngine {
   repo: Microcosm
-  registry: Registry
   domains: DomainList
+  lifecycle: Registry
+  registry: Registry
 
   constructor(repo: Microcosm) {
     this.repo = repo
-    this.registry = {}
-    this.domains = []
-
-    // All realms contain a meta domain for basic Microcosm operations
-    this.add([], MetaDomain)
-  }
-
-  getRepoHandlers(action: Action): Registrations {
-    let { command, status } = action
+    this.domains = {}
+    this.lifecycle = {}
 
-    let steps = getRegistration(result(this.repo, 'register'), command, status)
+    this.lifecycle[RESET.toString()] = []
+    this.lifecycle[PATCH.toString()] = []
 
-    return steps.length ? [{ key: [], scope: this.repo, steps }] : []
+    this.registry = Object.create(this.lifecycle)
   }
 
   getHandlers(action: Action): Registrations {
-    let handlers = this.getRepoHandlers(action)
-
     let { command, status } = action
 
-    for (var i = 0; i < this.domains.length; i++) {
-      var [key, scope] = this.domains[i]
+    let handlers = []
+
+    for (var key in this.domains) {
+      var scope = this.domains[key]
 
       let steps = getRegistration(result(scope, 'register'), command, status)
 
       if (steps.length) {
-        handlers.push({ key, scope, steps })
+        handlers.push({ key, scope, steps, local: false })
       }
     }
 
@@ -76,14 +69,14 @@ class DomainEngine {
     return this.registry[type]
   }
 
-  add(key: string | KeyPath, config: *, options?: Object) {
+  add(key: string, config: *, options?: Object) {
     console.assert(
       !options || options.constructor === Object,
-      'addDomain expected a plain object as the second argument.',
-      'Instead got',
-      get(options, 'constructor.name', 'Unknown')
+      'addDomain expected a plain object as the third argument.'
     )
 
+    console.assert(key && key.length > 0, 'Can not add domain to root level.')
+
     let deepOptions = merge(
       this.repo.options,
       config.defaults,
@@ -91,13 +84,35 @@ class DomainEngine {
       options
     )
 
-    let keyPath: KeyPath = castPath(key)
     let domain: Domain = createOrClone(config, deepOptions, this.repo)
 
-    this.domains.push([keyPath, domain])
-
-    // Reset the registry
-    this.registry = {}
+    this.domains[key] = domain
+
+    this.lifecycle[RESET.toString()].push({
+      key: key,
+      scope: domain,
+      local: true,
+      steps: [
+        (state: *, data: any) => {
+          return data[key] !== undefined
+            ? data[key]
+            : result(domain, 'getInitialState')
+        }
+      ]
+    })
+
+    this.lifecycle[PATCH.toString()].push({
+      key: key,
+      scope: domain,
+      local: true,
+      steps: [
+        (state: *, data: any) => {
+          return data[key] !== undefined ? data[key] : state
+        }
+      ]
+    })
+
+    this.registry = Object.create(this.lifecycle)
 
     if (domain.setup) {
       domain.setup(this.repo, deepOptions)
@@ -112,60 +127,78 @@ class DomainEngine {
 
   dispatch(action: Action, state: Object, snapshot: Snapshot) {
     let handlers = this.register(action)
-    let result = state
+    let answer = state
 
     for (var i = 0; i < handlers.length; i++) {
-      var { key, scope, steps } = handlers[i]
+      var { local, key, scope, steps } = handlers[i]
 
-      var base = get(result, key, null)
-      var head = get(snapshot.last, key, null)
+      if (local && action.origin !== this.repo) {
+        continue
+      }
+
+      var last = answer[key]
+      var head = snapshot.last[key]
+      var next = snapshot.next[key]
 
       if (
         // If the reference to the prior state changed
-        base !== head ||
+        last !== head ||
         // Or the payload is different
         action.payload !== snapshot.payload ||
         // or the status is different
         action.status !== snapshot.status
       ) {
-        // Yes: recalculate state from the base
-        result = set(result, key, reduce(steps, action.payload, base, scope))
-      } else {
-        // No: use the existing snapshot value (memoizing the domain handler)
-        result = set(result, key, get(snapshot.next, key))
+        // Recalculate state from the last answer
+        next = reduce(steps, action.payload, last, scope)
+      }
+
+      if (answer === state && next !== answer[key]) {
+        answer = clone(state)
       }
+
+      answer[key] = next
     }
 
-    return result
+    return answer
   }
 
-  deserialize(payload: Object): Object {
-    let next = payload
+  deserialize(data: Object): Object {
+    let next = clone(data)
 
-    for (var i = 0; i < this.domains.length; i++) {
-      var [key, domain] = this.domains[i]
+    for (var key in this.domains) {
+      var domain = this.domains[key]
 
       if (domain.deserialize) {
-        next = set(next, key, domain.deserialize(get(payload, key)))
+        next[key] = domain.deserialize(data[key])
       }
     }
 
     return next
   }
 
-  serialize(state: Object, payload: Object): Object {
-    let next = payload
+  serialize(state: Object, data: Object): Object {
+    let next = clone(data)
 
-    for (var i = 0; i < this.domains.length; i++) {
-      var [key, domain] = this.domains[i]
+    for (var key in this.domains) {
+      var domain = this.domains[key]
 
       if (domain.serialize) {
-        next = set(next, key, domain.serialize(get(state, key)))
+        next[key] = domain.serialize(state[key])
       }
     }
 
     return next
   }
+
+  getInitialState(): Object {
+    let next = {}
+
+    for (var key in this.domains) {
+      next[key] = result(this.domains[key], 'getInitialState')
+    }
+
+    return next
+  }
 }
 
 export default DomainEngine
diff --git a/packages/microcosm/src/get-registration.js b/packages/microcosm/src/get-registration.js
index 74e06020..041288eb 100644
--- a/packages/microcosm/src/get-registration.js
+++ b/packages/microcosm/src/get-registration.js
@@ -19,6 +19,7 @@ function getRegistration(pool: any, command: Tagged, status: Status) {
   let alias = STATUS[status]
 
   console.assert(alias, 'Invalid action status ' + status)
+
   console.assert(
     command.__tagged,
     `Unable to register ${command.name || 'action'}(). It has not been tagged.`
@@ -54,8 +55,7 @@ function getRegistration(pool: any, command: Tagged, status: Status) {
      */
     console.assert(
       !(type in pool) || answer !== undefined,
-      `${command.name ||
-        'action'} key within a registration is ${answer}. Is it being referenced correctly?`
+      `${command.toString()} key within a registration is ${answer}. Is it being referenced correctly?`
     )
   }
 
diff --git a/packages/microcosm/src/history.js b/packages/microcosm/src/history.js
index 38ccb111..316be015 100644
--- a/packages/microcosm/src/history.js
+++ b/packages/microcosm/src/history.js
@@ -10,7 +10,7 @@ import Action from './action'
 import Emitter from './emitter'
 import defaultUpdateStrategy from './default-update-strategy'
 import { merge } from './utils'
-import { BIRTH, START, ADD_DOMAIN } from './lifecycle'
+import { BIRTH, START } from './lifecycle'
 import { type Updater } from './default-update-strategy'
 import { iteratorTag } from './symbols'
 
@@ -134,12 +134,7 @@ class History extends Emitter {
 
         // Ignore certain lifecycle actions that are only for
         // internal purposes
-        if (
-          next &&
-          (next.command === BIRTH ||
-            next.command === START ||
-            next.command === ADD_DOMAIN)
-        ) {
+        if (next && (next.command === BIRTH || next.command === START)) {
           return iterator.next()
         }
 
@@ -279,6 +274,13 @@ class History extends Emitter {
     action.remove()
   }
 
+  /**
+   * Re-reconcile the last action. Used by Microcosms to "refresh" their snapshot
+   */
+  refresh() {
+    this.reconcile(this.head)
+  }
+
   /**
    * Starting with a given action, emit events such that repos can
    * dispatch actions to domains in a consistent order to build a new
diff --git a/packages/microcosm/src/lifecycle.js b/packages/microcosm/src/lifecycle.js
index 018d4ef1..54a9d245 100644
--- a/packages/microcosm/src/lifecycle.js
+++ b/packages/microcosm/src/lifecycle.js
@@ -2,7 +2,6 @@
  * @flow
  */
 
-import { mergeSame } from './utils'
 import tag from './tag'
 
 import type Action from './action'
@@ -20,22 +19,15 @@ function sandbox(data: Object, deserialize: boolean) {
         throw error
       }
     }
-    // Strip out keys not managed by this repo. This prevents children from
-    // accidentally having their keys reset by parents.
-    action.resolve(mergeSame(repo.state, payload))
+
+    action.resolve(payload || {})
   }
 }
 
-export const RESET = function $reset(data: Object, deserialize: boolean) {
-  return sandbox(data, deserialize)
-}
+export const RESET = tag(sandbox.bind(null), '$reset')
 
-export const PATCH = function $patch(data: Object, deserialize: boolean) {
-  return sandbox(data, deserialize)
-}
+export const PATCH = tag(sandbox.bind(null), '$patch')
 
 export const BIRTH = tag('$birth')
 
 export const START = tag('$start')
-
-export const ADD_DOMAIN = tag('$addDomain')
diff --git a/packages/microcosm/src/meta-domain.js b/packages/microcosm/src/meta-domain.js
deleted file mode 100644
index 9176ff7d..00000000
--- a/packages/microcosm/src/meta-domain.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * @fileoverview Every Microcosm includes MetaDomain. It provides the
- * plumbing required for lifecycle methods like `reset` and `patch`.
- * @flow
- */
-
-import { mergeSame, merge } from './utils'
-import { RESET, PATCH, ADD_DOMAIN } from './lifecycle'
-
-class MetaDomain {
-  repo: Microcosm
-
-  setup(repo: Microcosm) {
-    this.repo = repo
-  }
-
-  /**
-   * Build a new Microcosm state object.
-   */
-  reset(oldState: Object, newState: Object): Object {
-    let filtered = mergeSame(oldState, newState)
-
-    return merge(oldState, this.repo.getInitialState(), filtered)
-  }
-
-  /**
-   * Merge a state object into the current Microcosm state.
-   */
-  patch(oldState: Object, newState: Object): Object {
-    let filtered = mergeSame(oldState, newState)
-
-    return merge(oldState, filtered)
-  }
-
-  /**
-   * Update the initial state whenever a new domain is added to a
-   * repo.
-   */
-  addDomain(oldState: Object): Object {
-    return merge(this.repo.getInitialState(), oldState)
-  }
-
-  register() {
-    // TODO: Flow does not like string coercion. How can we
-    // get Flow type coverage on the register method?
-    var registry = {
-      [RESET.toString()]: this.reset,
-      [PATCH.toString()]: this.patch,
-      [ADD_DOMAIN.toString()]: this.addDomain
-    }
-
-    return registry
-  }
-}
-
-export default MetaDomain
diff --git a/packages/microcosm/src/microcosm.js b/packages/microcosm/src/microcosm.js
index f1f4d32f..2e136e98 100644
--- a/packages/microcosm/src/microcosm.js
+++ b/packages/microcosm/src/microcosm.js
@@ -11,8 +11,8 @@ import DomainEngine from './domain-engine'
 import EffectEngine from './effect-engine'
 import tag from './tag'
 import installDevtools from './install-devtools'
-import { RESET, PATCH, ADD_DOMAIN } from './lifecycle'
-import { merge, set } from './utils'
+import { RESET, PATCH } from './lifecycle'
+import { merge } from './utils'
 import { version } from '../package.json'
 
 /**
@@ -27,6 +27,8 @@ const DEFAULTS = {
   debug: false
 }
 
+const EMPTY = {}
+
 /**
  * # Overview
  *
@@ -86,7 +88,7 @@ const DEFAULTS = {
  * @extends Emitter
  * @tutorial quickstart
  */
-class Microcosm extends Emitter implements Domain {
+class Microcosm extends Emitter {
   static defaults: Object
   static version: String
 
@@ -108,7 +110,6 @@ class Microcosm extends Emitter implements Domain {
 
     this.active = true
     this.parent = this.options.parent
-    this.initial = this.parent ? this.parent.initial : this.getInitialState()
     this.snapshots = Object.create(this.parent ? this.parent.snapshots : null)
     this.history = this.parent ? this.parent.history : new History(this.options)
     this.actions = Object.create(this.parent ? this.parent.actions : null)
@@ -180,7 +181,9 @@ class Microcosm extends Emitter implements Domain {
    * 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 == null ? {} : this.initial
+    let base = this.domains ? this.domains.getInitialState() : EMPTY
+
+    return this.parent ? merge(this.parent.getInitialState(), base) : base
   }
 
   /**
@@ -245,16 +248,19 @@ class Microcosm extends Emitter implements Domain {
    * provided options and associated repo.
    */
   addDomain(key: string, config: *, options?: Object) {
+    if (key in this.state) {
+      throw new Error(
+        `Can not add domain for "${key}". This state is already managed.`
+      )
+    }
+
     this._enableDomains()
 
     let domain = this.domains.add(key, config, options)
-    let initial = domain.getInitialState ? domain.getInitialState() : null
-
-    this.initial = set(this.initial, key, initial)
 
     this._alias(domain.actions)
 
-    this.push(ADD_DOMAIN, domain)
+    this.history.refresh()
 
     return domain
   }
@@ -293,13 +299,6 @@ class Microcosm extends Emitter implements Domain {
     return this.push(PATCH, data, deserialize)
   }
 
-  /**
-   * The default registration method for Microcosms
-   */
-  register() {
-    return null
-  }
-
   /**
    * For each key in the provided data parameter, transform it using
    * the deserialize method provided by the domain managing that
@@ -411,18 +410,18 @@ class Microcosm extends Emitter implements Domain {
   _updateSnapshot(action: Action) {
     let snap = this.snapshots[action.id]
     let last = this._rebase(action)
+    let next = last
 
     if (!action.disabled) {
-      snap.next = this.domains.dispatch(action, last, snap)
-    } else {
-      snap.next = last
+      next = this.domains.dispatch(action, last, snap)
     }
 
-    this.snapshots[action.id] = merge(snap, {
+    this.snapshots[action.id] = {
+      next: next,
       last: last,
       status: action.status,
       payload: action.payload
-    })
+    }
   }
 
   _removeSnapshot(action: Action) {
@@ -430,10 +429,15 @@ class Microcosm extends Emitter implements Domain {
   }
 
   _didChange(): boolean {
-    let head = this.history.head
-    let snap = this.snapshots[head.id]
+    let key = this.history.head.id
+
+    if (key in this.snapshots) {
+      let snap = this.snapshots[key]
+
+      return snap.last !== snap.next
+    }
 
-    return snap && snap.last !== snap.next
+    return false
   }
 
   _recall(action: ?Action): Object {
diff --git a/packages/microcosm/src/utils.js b/packages/microcosm/src/utils.js
index ae7e6e6c..e31fa42d 100644
--- a/packages/microcosm/src/utils.js
+++ b/packages/microcosm/src/utils.js
@@ -16,22 +16,6 @@ export function uid(prefix: string): string {
   return `${prefix}${uidStepper++}`
 }
 
-/**
- * Merge on object into the next, but only assign keys that are
- * defined in the base.
- */
-export function mergeSame<T: MixedObject>(target: T, head: Object): $Shape<T> {
-  let next = {}
-
-  for (var key in head) {
-    if (key in target) {
-      next[key] = head[key]
-    }
-  }
-
-  return next
-}
-
 /**
  * Shallow copy an object
  */


Test patch:
diff --git a/packages/microcosm-preact/test/unit/presenter.test.js b/packages/microcosm-preact/test/unit/presenter.test.js
index 4d45dd61..5f2007f0 100644
--- a/packages/microcosm-preact/test/unit/presenter.test.js
+++ b/packages/microcosm-preact/test/unit/presenter.test.js
@@ -10,8 +10,12 @@ const View = withSend(function({ send }) {
 })
 
 class Repo extends Microcosm {
-  getInitialState() {
-    return { color: 'yellow' }
+  setup() {
+    this.addDomain('color', {
+      getInitialState() {
+        return 'yellow'
+      }
+    })
   }
 }
 
diff --git a/packages/microcosm/test/addons/compare-tree/compare-tree.test.js b/packages/microcosm/test/addons/compare-tree/compare-tree.test.js
index afb8048d..10909e17 100644
--- a/packages/microcosm/test/addons/compare-tree/compare-tree.test.js
+++ b/packages/microcosm/test/addons/compare-tree/compare-tree.test.js
@@ -7,8 +7,6 @@ class Repo extends Microcosm {
     this.addDomain('meta', {})
     this.addDomain('planets', {})
     this.addDomain('physics', {})
-
-    this.reset(SOLAR_SYSTEM)
   }
 }
 
@@ -17,7 +15,7 @@ describe('CompareTree', function() {
   let repo = null
 
   beforeEach(function() {
-    repo = new Repo()
+    repo = new Repo({}, SOLAR_SYSTEM)
     tree = new CompareTree(repo.state)
 
     repo.on('change', tree.update, tree)
@@ -219,11 +217,13 @@ describe('CompareTree', function() {
 
       repo.reset({ meta: null })
 
+      expect(handler).toHaveBeenCalledWith(null)
+
       repo.reset({
         meta: { selected: true }
       })
 
-      expect(handler).toHaveBeenCalledWith(null)
+      expect(handler).toHaveBeenCalledWith(true)
     })
 
     it('the root node does not get called twice if subscribing to two children', function() {
diff --git a/packages/microcosm/test/addons/indexes.test.js b/packages/microcosm/test/addons/indexes.test.js
index 40332cc1..ae72d4e8 100644
--- a/packages/microcosm/test/addons/indexes.test.js
+++ b/packages/microcosm/test/addons/indexes.test.js
@@ -82,7 +82,7 @@ describe('indexing', function() {
     let a = repo.compute('styles')
     let b = child.compute('styles')
 
-    expect(a).toBe(b)
+    expect(a).toEqual(b)
   })
 })
 
diff --git a/packages/microcosm/test/addons/presenter.test.js b/packages/microcosm/test/addons/presenter.test.js
index ed96cc24..490825b6 100644
--- a/packages/microcosm/test/addons/presenter.test.js
+++ b/packages/microcosm/test/addons/presenter.test.js
@@ -17,8 +17,12 @@ const View = withSend(function({ send }) {
 const timer = time => new Promise(resolve => setTimeout(resolve, time))
 
 class Repo extends Microcosm {
-  getInitialState() {
-    return { color: 'yellow' }
+  setup() {
+    this.addDomain('color', {
+      getInitialState() {
+        return 'yellow'
+      }
+    })
   }
 }
 
diff --git a/packages/microcosm/test/integration/mutation.test.js b/packages/microcosm/test/integration/mutation.test.js
index 85f8133c..31a90f9e 100644
--- a/packages/microcosm/test/integration/mutation.test.js
+++ b/packages/microcosm/test/integration/mutation.test.js
@@ -5,12 +5,12 @@ describe('Mutations', function() {
     const identity = function() {}
     const repo = new Microcosm({ maxHistory: Infinity })
 
-    repo.addDomain(null, {
+    repo.addDomain('test', {
       getInitialState() {
-        return { test: false }
+        return { key: false }
       },
       mutate(state) {
-        state.test = true
+        state.key = true
         return state
       },
       register() {
@@ -22,6 +22,6 @@ describe('Mutations', function() {
 
     repo.push(identity, true)
 
-    expect(repo).toHaveState('test', true)
+    expect(repo).toHaveState('test.key', true)
   })
 })
diff --git a/packages/microcosm/test/integration/reconciliation.test.js b/packages/microcosm/test/integration/reconciliation.test.js
index 92128fa3..467e283b 100644
--- a/packages/microcosm/test/integration/reconciliation.test.js
+++ b/packages/microcosm/test/integration/reconciliation.test.js
@@ -3,7 +3,7 @@ import Microcosm from 'microcosm'
 describe('Reconciliation', function() {
   const action = n => n
 
-  it('only interates over the point of reconciliation', function() {
+  it('only iterates over the point of reconciliation', function() {
     const repo = new Microcosm({ maxHistory: Infinity })
     const handler = jest.fn((a, b) => a + b)
 
@@ -23,6 +23,7 @@ describe('Reconciliation', function() {
     expect(repo).toHaveState('count', 0)
 
     one.resolve(1)
+
     expect(repo).toHaveState('count', 1)
 
     two.resolve(2)
diff --git a/packages/microcosm/test/unit/domain/construction.test.js b/packages/microcosm/test/unit/domain/construction.test.js
index 353537e5..9ab813a3 100644
--- a/packages/microcosm/test/unit/domain/construction.test.js
+++ b/packages/microcosm/test/unit/domain/construction.test.js
@@ -5,9 +5,7 @@ describe('Domain construction', function() {
     expect(function() {
       let repo = new Microcosm()
       repo.addDomain('foo', {}, new Microcosm())
-    }).toThrow(
-      /expected a plain object as the second argument\. Instead got Microcosm/
-    )
+    }).toThrow('addDomain expected a plain object as the third argument.')
   })
 
   it('object - primitive', function() {
diff --git a/packages/microcosm/test/unit/domain/deserialize.test.js b/packages/microcosm/test/unit/domain/deserialize.test.js
index 5b1f8f5d..1dc7fa4d 100644
--- a/packages/microcosm/test/unit/domain/deserialize.test.js
+++ b/packages/microcosm/test/unit/domain/deserialize.test.js
@@ -46,22 +46,5 @@ describe('Domain::deserialize', function() {
 
       expect(data).toEqual({ fiz: 'fiz', buzz: 'buzz' })
     })
-
-    it('deserializing a child with the same key works from serialization', function() {
-      const parent = new Microcosm()
-      const child = parent.fork()
-
-      parent.addDomain('fiz', {
-        deserialize: word => word + '-first'
-      })
-
-      child.addDomain('fiz', {
-        deserialize: word => word + '-second'
-      })
-
-      const data = child.deserialize({ fiz: 'fiz' })
-
-      expect(data).toEqual({ fiz: 'fiz-first-second' })
-    })
   })
 })
diff --git a/packages/microcosm/test/unit/domain/serialize.test.js b/packages/microcosm/test/unit/domain/serialize.test.js
index 00a8ba31..0f623635 100644
--- a/packages/microcosm/test/unit/domain/serialize.test.js
+++ b/packages/microcosm/test/unit/domain/serialize.test.js
@@ -57,21 +57,5 @@ describe('Domain::serialize', function() {
 
       expect(child.serialize()).toEqual({ fiz: 'FIZ', buzz: 'BUZZ' })
     })
-
-    it('serializing a child with the same key works from that state', function() {
-      const parent = new Microcosm()
-      const child = parent.fork()
-
-      parent.addDomain('fiz', {
-        getInitialState: () => 'fiz',
-        serialize: word => word + '-first'
-      })
-
-      child.addDomain('fiz', {
-        serialize: word => word + '-second'
-      })
-
-      expect(child.serialize()).toEqual({ fiz: 'fiz-second' })
-    })
   })
 })
diff --git a/packages/microcosm/test/unit/microcosm/addDomain.test.js b/packages/microcosm/test/unit/microcosm/addDomain.test.js
index 41d000a2..f4987d16 100644
--- a/packages/microcosm/test/unit/microcosm/addDomain.test.js
+++ b/packages/microcosm/test/unit/microcosm/addDomain.test.js
@@ -22,7 +22,30 @@ describe('Microcosm::addDomain', function() {
     expect(repo).toHaveState('b', 1)
   })
 
+  it.dev('can not add domains to the root', function() {
+    let repo = new Microcosm()
+
+    expect(() => repo.addDomain('', {})).toThrow(
+      'Can not add domain to root level.'
+    )
+  })
+
   describe('forks', function() {
+    it('receives initial state from parents', function() {
+      let repo = new Microcosm()
+
+      repo.addDomain('a', {
+        getInitialState() {
+          return 0
+        }
+      })
+
+      let fork = repo.fork()
+
+      expect(repo).toHaveState('a', 0)
+      expect(fork).toHaveState('a', 0)
+    })
+
     it('adding a domain to a fork does not reset all state', function() {
       let repo = new Microcosm()
 
@@ -94,5 +117,16 @@ describe('Microcosm::addDomain', function() {
 
       expect(repo).not.toHaveState('b')
     })
+
+    it('can not add domains to keys managed by parents', function() {
+      let repo = new Microcosm()
+      let fork = repo.fork()
+
+      repo.addDomain('test', {})
+
+      expect(() => fork.addDomain('test', {})).toThrow(
+        'Can not add domain for "test". This state is already managed.'
+      )
+    })
   })
 })
diff --git a/packages/microcosm/test/unit/microcosm/dispatch.test.js b/packages/microcosm/test/unit/microcosm/dispatch.test.js
index 5f90fde3..90b1d344 100644
--- a/packages/microcosm/test/unit/microcosm/dispatch.test.js
+++ b/packages/microcosm/test/unit/microcosm/dispatch.test.js
@@ -5,7 +5,7 @@ describe('Microcosm::dispatch', function() {
     const repo = new Microcosm()
     const mutation = () => true
 
-    repo.addDomain(null, {
+    repo.addDomain('ui', {
       getInitialState() {
         return {
           toggled: false
@@ -24,15 +24,15 @@ describe('Microcosm::dispatch', function() {
 
     repo.push(mutation)
     expect(repo.history.size).toEqual(1)
-    expect(repo).toHaveState('toggled', true)
+    expect(repo).toHaveState('ui.toggled', true)
 
     repo.push(mutation)
     expect(repo.history.size).toEqual(1)
-    expect(repo).toHaveState('toggled', false)
+    expect(repo).toHaveState('ui.toggled', false)
 
     repo.push(mutation)
     expect(repo.history.size).toEqual(1)
-    expect(repo).toHaveState('toggled', true)
+    expect(repo).toHaveState('ui.toggled', true)
   })
 
   it('does not retroactively apply old state to subsequent domain handlers', function() {
@@ -51,15 +51,13 @@ describe('Microcosm::dispatch', function() {
       }
     })
 
-    repo.addDomain(null, {
+    repo.addEffect({
       register() {
         return {
-          test: state => {
+          test: repo => {
             // Assert that domains receive the result of
             // earlier domain processing
-            expect(state.color).toEqual('purple')
-
-            return state
+            expect(repo).toHaveState('color', 'purple')
           }
         }
       }
diff --git a/packages/microcosm/test/unit/microcosm/fork.test.js b/packages/microcosm/test/unit/microcosm/fork.test.js
index e109a208..7510c3bf 100644
--- a/packages/microcosm/test/unit/microcosm/fork.test.js
+++ b/packages/microcosm/test/unit/microcosm/fork.test.js
@@ -1,38 +1,20 @@
 import Microcosm from 'microcosm'
 
 describe('Microcosm::fork', function() {
-  it('forks do not own state of parents', () => {
+  it('forks can not manage state owned by parents', () => {
     const parent = new Microcosm()
     const child = parent.fork()
 
-    const add = n => n
-
-    parent.addDomain('counter', {
-      getInitialState() {
-        return 0
-      },
-      register() {
-        return {
-          [add]: (a, b) => a + b
-        }
-      }
-    })
+    parent.addDomain('counter', {})
 
-    child.addDomain('counter', {
-      register() {
-        return {
-          [add]: (a, b) => a * 2
-        }
-      }
-    })
-
-    parent.push(add, 2)
-
-    expect(parent.state.counter).toEqual(2)
-    expect(child.state.counter).toEqual(4)
+    expect(function() {
+      child.addDomain('counter', {})
+    }).toThrow(
+      'Can not add domain for "counter". This state is already managed.'
+    )
   })
 
-  it('forks continue to get updates from their parents when there is no archive', () => {
+  it('recieve upstream state updates when they push actions', () => {
     const parent = new Microcosm({ maxHistory: Infinity })
     const child = parent.fork()
 
@@ -49,64 +31,14 @@ describe('Microcosm::fork', function() {
       }
     })
 
-    child.addDomain('counter', {
-      register() {
-        return {
-          [add](a) {
-            return a * 2
-          }
-        }
-      }
-    })
-
-    child.push(add, 2)
-    child.push(add, 4)
-
-    expect(parent.state.counter).toEqual(6)
-
-    // If this is 24, then multiplcation applied twice on 6,
-    // rather than multiply 6 by 2
-    expect(child.state.counter).toEqual(12)
-  })
-
-  it('forks handle async', () => {
-    const parent = new Microcosm()
-    const child = parent.fork()
-
-    const add = n => n
-
-    parent.addDomain('counter', {
-      getInitialState() {
-        return 0
-      },
-      register() {
-        return {
-          [add]: (a, b) => a + b
-        }
-      }
-    })
-
-    child.addDomain('counter', {
-      register() {
-        return {
-          [add](a) {
-            return a * 2
-          }
-        }
-      }
-    })
-
     child.push(add, 2)
     child.push(add, 4)
 
-    expect(parent.state.counter).toEqual(6)
-
-    // If this is 24, then multiplcation applied twice on 6,
-    // rather than multiply 6 by 2
-    expect(child.state.counter).toEqual(12)
+    expect(parent).toHaveState('counter', 6)
+    expect(child).toHaveState('counter', 6)
   })
 
-  it('forks handle cases where they are lostlost during a reconcilation', function() {
+  it('forks handle cases where they are lost during a reconcilation', function() {
     const parent = new Microcosm()
     const child = parent.fork()
 
diff --git a/packages/microcosm/test/unit/microcosm/patch.test.js b/packages/microcosm/test/unit/microcosm/patch.test.js
index 77c775a1..4891197e 100644
--- a/packages/microcosm/test/unit/microcosm/patch.test.js
+++ b/packages/microcosm/test/unit/microcosm/patch.test.js
@@ -11,20 +11,6 @@ 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()
 
@@ -117,10 +103,9 @@ describe('Microcosm::patch', function() {
 
       parent.addDomain('top', {
         getInitialState() {
-          return false
+          return true
         }
       })
-
       child.addDomain('bottom', {
         getInitialState() {
           return false
@@ -129,8 +114,9 @@ describe('Microcosm::patch', function() {
 
       child.patch({ bottom: true })
 
-      expect(child).toHaveState('top', false)
-      expect(child).toHaveState('bottom', true)
+      expect(parent.state).toEqual({ top: true })
+
+      expect(child.state).toEqual({ top: true, bottom: true })
     })
 
     it('does not strip away parent inherited state when the parent patches', function() {
diff --git a/packages/microcosm/test/unit/microcosm/reset.test.js b/packages/microcosm/test/unit/microcosm/reset.test.js
index fcae0803..2a8f6046 100644
--- a/packages/microcosm/test/unit/microcosm/reset.test.js
+++ b/packages/microcosm/test/unit/microcosm/reset.test.js
@@ -51,7 +51,7 @@ describe('Microcosm::reset', function() {
 
     // This is invalid
     try {
-      repo.reset('{ test: deserialize }', true)
+      repo.reset('{ test: badJson }', true)
     } catch (x) {
       // do not handle this error
     }
diff --git a/packages/microcosm/test/unit/registration.test.js b/packages/microcosm/test/unit/registration.test.js
index b9aef692..25dc1bba 100644
--- a/packages/microcosm/test/unit/registration.test.js
+++ b/packages/microcosm/test/unit/registration.test.js
@@ -7,18 +7,20 @@ describe('getRegistration', function() {
     let handler = jest.fn()
 
     class Repo extends Microcosm {
-      register() {
-        return {
-          [action]: {
-            reject: handler
+      setup() {
+        this.addDomain('test', {
+          register: {
+            action: {
+              reject: handler
+            }
           }
-        }
+        })
       }
     }
 
     let repo = new Repo()
 
-    repo.append(action).reject()
+    repo.append('action').reject()
 
     expect(handler).toHaveBeenCalled()
   })
@@ -68,11 +70,15 @@ describe('getRegistration', function() {
   it.dev('throws if given an invalid status', function() {
     let repo = new Microcosm()
 
-    let fail = function() {
-      repo.append(action, 'totally-missing')
-    }
+    // Registration does not happen if there are no registerable
+    // domains
+    repo.addDomain('test', {
+      register: {}
+    })
 
-    expect(fail).toThrow('Invalid action status totally-missing')
+    expect(() => {
+      repo.append(action, 'totally-missing')
+    }).toThrow('Invalid action status totally-missing')
   })
 
   describe('Action aliasing', function() {
@@ -87,18 +93,20 @@ describe('getRegistration', function() {
         let handler = jest.fn()
 
         class Repo extends Microcosm {
-          register() {
-            return {
-              [action]: {
-                [status]: handler
+          setup() {
+            this.addDomain('test', {
+              register: {
+                action: {
+                  [status]: handler
+                }
               }
-            }
+            })
           }
         }
 
         let repo = new Repo()
 
-        repo.append(action, status)
+        repo.append('action', status)
 
         expect(handler).toHaveBeenCalled()
       })
@@ -107,18 +115,20 @@ describe('getRegistration', function() {
         let handler = jest.fn()
 
         class Repo extends Microcosm {
-          register() {
-            return {
-              [action]: {
-                [alias]: handler
+          setup() {
+            this.addDomain('test', {
+              register: {
+                action: {
+                  [status]: handler
+                }
               }
-            }
+            })
           }
         }
 
         let repo = new Repo()
 
-        repo.append(action, status)
+        repo.append('action', status)
 
         expect(handler).toHaveBeenCalled()
       })
@@ -128,19 +138,19 @@ describe('getRegistration', function() {
   it.dev(
     'prints the action name in the warning when a handler is undefined',
     function() {
-      let getUser = n => n
-
       class Repo extends Microcosm {
-        register() {
-          return {
-            [getUser]: undefined
-          }
+        setup() {
+          this.addDomain('test', {
+            register: {
+              getUser: undefined
+            }
+          })
         }
       }
 
       let repo = new Repo()
 
-      expect(repo.prepare(getUser)).toThrow(
+      expect(repo.prepare('getUser')).toThrow(
         'getUser key within a registration is undefined. Is it being referenced correctly?'
       )
     }