Note: This is a work in progress document and feature. History has been a private API for a very long time. We're still working through the best public interface for working with Microcosm's action history.
All Microcosms have a history. This history keeps track of outstanding actions, working with a Microcosm to determine the next application state as actions move through different states.
By default, Microcosm automatically disposes actions that are
complete. However you can control how many actions Microcosm's history
will hold on to by passing the maxHistory
option:
// Hold onto the last 10 actions
let repo = new Microcosm({ maxHistory: 10 })
// Or never forget anything
let repo = new Microcosm({ maxHistory: Infinity })
// Then push some actions
repo.push(action)
repo.push(action)
repo.push(action)
// Get actions out by using `repo.history.toArray()`
let actions = repo.history.toArray() // [actionOne, actionTwo, actionThree]
History stores actions as a tree, with a root and a head:
[root] - [one] - [two] - [three]
You can revert to a prior action by using checkout(action)
:
let repo = new Microcosm({ maxHistory: Infinity })
let one = repo.push(action)
let two = repo.push(action)
repo.history.checkout(one)
let three = repo.push(action)
In the example above, the following tree would be produced:
|- [two]
[root] - [one] +
|- [*three]
three
is the active branch, marked with an asterisk. The active
branch is used to determine state. Here, a Microcosm's state will be
the result of dispatching root
, one
, and three
to Domains.
What makes History such a powerful feature of Microcosm is its ability to walk through recorded actions whenever there is a change, similar to a rebase in git.
Let's say we have four actions pushed to our History, and that three
of the four are completed (C)
, but the second action we pushed is
taking a long time and is still open (O)
.
[root] - [one](C) - [two](O) - [three](C) - [four](C)
When our two
action here resolves or rejects, History will initiate
a reconciliation starting at two
, and walk forward to three
and
then four
. At each step, your Domains will recompute changes to the
state based on the actions.
This ensures that your application state is always accurate based on the order in which actions were triggered.
Set the head of the tree to a target action. This has the effect of controlling time in a Microcosm's history:
let repo = new Microcosm({ maxHistory: Infinity })
let one = repo.push(action)
let two = repo.push(action)
repo.history.checkout(one)
let three = repo.push(action)
The head of the history tree above is now three. Microcosm will
calculate state by reconciling root
, one,
and three
.
Disable a group of actions:
let repo = new Microcosm({ maxHistory: Infinity })
let one = repo.push(addUser)
let two = repo.push(updateUser)
repo.history.toggle([one, two])
In the example above, actions one
and two
will be forgotten. The
repo's history will reconcile, and it will be as if they never
existed.
This flips the disabled
state of each action provided. By executing
toggle a second time, these actions will be re-enabled:
// Actions disabled in the prior example
repo.history.toggle([one, two])
// `one` and `two` have been re-enabled
Return an array version of the active branch:
let repo = new Microcosm({ maxHistory: Infinity })
let one = repo.push(action)
let two = repo.push(action)
repo.history.checkout(one)
let three = repo.push(action)
repo.toArray() // => [ root, one, three ]
Return a Promise that waits for all current actions to complete. If any action rejects, this promise will reject. cancellation is ignored. Cancelling an action will not reject this promise.
let repo = new Microcosm({ maxHistory: Infinity })
let one = repo.push(asyncAction)
let two = repo.push(asyncAction)
let three = repo.push(asyncAction)
repo.wait().then(function() {
// Everything is done
})
Allows for direct Promise interop with history. One common use case for this is to wait for all actions to complete before executing a test. For example, if we were to write a test with Jest:
describe('An AJAX behavior', function() {
it('adds a user', async function() {
let repo = new MyMicrocosm({ maxHistory: Infinity })
repo.push(getUser, 1)
repo.push(getUser, 2)
repo.push(getUser, 3)
await repo.history
expect(repo.users.length).toEqual(3)
})
})
Completely remove an action from history. This is a dangerous operation! Removed actions can never be re-inserted.
let repo = new MyMicrocosm({ maxHistory: Infinity })
let one = repo.push(action)
let two = repo.push(action)
let three = repo.push(action)
repo.history.remove(two)
repo.history.toArray() // [root, one, three]
History emits events any time something of interest happens. This is how Microcosm knows to update the state to accurately reflect what's going on given a sequence of actions and their statuses.
You can manage event listeners with the following methods.
Adds an event listener to a Microcosm History instance.
const repo = new Microcosm()
const history = repo.history
history.on('append', callback)
Removes an event listener.
history.off('append', callback)
Arguments: action
Emitted when an action is pushed onto the History stack.
history.on('append', function(action) {
console.log('Action pushed:', action.id)
})
repo.push(newAction)
// Action pushed: 42
Arguments: action
Emitted when an action is removed from the History stack.
history.on('remove', function(action) {
console.log('Action removed:', action.id)
})
action = repo.push(newAction)
history.remove(action)
// Action removed: 42
Arguments: action
Whenever there is an update to an action's status, an update
event
is emitted with that action.
history.on('update', function(action) {
console.log('Action status:', action.status)
})
repo.push(newAction)
// Action status: inactive
// Action status: open
// Action status: update
// Action status: [resolve, reject, cancel]
Whenever an action that precedes other actions has a status update,
History walks forward in time from that action reconciling the
application state (see Reconciling for more details.)
For every action in this process, update
will be emitted.
Arguments: action
In response to an action's status changing, History triggers a
reconciliation. Once that has completed (and an update
event has
been emitted for each reconciled action), reconcile
is emitted with
the action that triggered the walk through.
history.on('reconcile', function(action) {
console.log('Action:', action.id)
})
repo.push(newAction)
// Action: 42
Emitted after a reconciliation pass. In setting up a Microcosm, you
have the option to pass a batch
option which will cause release
to be emitted in batched intervals (used internally to improve state
comparison performance).
let repo = new Microcosm({ batch: true })
let history = repo.history
history.on('append', function(action) {
console.log('Action:', action.id)
})
history.on('release', function() {
console.log('Released!')
})
repo.push(newAction)
repo.push(newAction)
repo.push(newAction)
// Action: 1
// Action: 2
// Action: 3
// Released!