leopold
Version:
Event-sourced state support
696 lines (679 loc) • 24.1 kB
HTML
<html>
<head>
<meta name="viewport" content="width=device-width" charset="utf-8">
<title>leopold</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/cayman.min.css">
<link rel="stylesheet" href="css/prism.min.css">
<link rel="stylesheet" href="css/index.min.css">
<link rel="stylesheet" href="css/docs.min.css">
<link rel="stylesheet" href="css/bootstrap-responsive.min.css">
</head>
<body data-spy="scroll" data-target=".scrollspy">
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container"><a class="brand">Mr. Doc</a>
<div class="nav-collapse collapse">
<ul class="nav pull-right sponsored"></ul>
</div>
</div>
</div>
</div>
<header id="overview" class="jumbotron subhead">
<div class="container">
<h1>leopold</h1>
<p class="lead"></p>
</div>
</header>
<div class="container">
<div class="row">
<div class="span3 bs-docs-sidebar">
<ul class="nav nav-list bs-docs-sidenav affix-top">
<li><a href="index.html">Main</a></li>
<li class="active"><a href="leopold.js.html">leopold.js</a></li>
</ul>
<div class="scrollspy">
<ul class="nav nav-list bs-docs-sidenav affix-top">
<li><a href="#identifiable"><i class="alert alert-success"></i><span>identifiable</span></a>
</li>
<li><a href="#revisable"><i class="alert alert-success"></i><span>revisable</span></a>
</li>
<li><a href="#revision"><i class="alert alert-info"></i><span>revision</span></a>
</li>
<li><a href="#nextRevision"><i class="alert alert-success"></i><span>nextRevision</span></a>
</li>
<li><a href="#hashIdentityMap"><i class="alert alert-success"></i><span>hashIdentityMap</span></a>
</li>
<li><a href="#clear"><i class="alert alert-success"></i><span>clear</span></a>
</li>
<li><a href="#raise"><i class="alert alert-success"></i><span>raise</span></a>
</li>
<li><a href="#pushEvent"><i class="alert alert-success"></i><span>pushEvent</span></a>
</li>
<li><a href="#nullStorage"><i class="alert alert-success"></i><span>nullStorage</span></a>
</li>
<li><a href="#atomic"><i class="alert alert-success"></i><span>atomic</span></a>
</li>
<li><a href="#eventable"><i class="alert alert-success"></i><span>eventable</span></a>
</li>
<li><a href="#commit"><i class="alert alert-success"></i><span>commit</span></a>
</li>
<li><a href="#unitOfWork"><i class="alert alert-success"></i><span>unitOfWork</span></a>
</li>
<li><a href="#mount"><i class="alert alert-success"></i><span>mount</span></a>
</li>
<li><a href="#restore"><i class="alert alert-success"></i><span>restore</span></a>
</li>
</ul>
</div>
</div>
<div class="span9">
<section id="identifiable">
<h1>identifiable</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">declaration</div><span> </span><span>identifiable</span><span> </span>
</p>
</section>
<div class="description"><p>accepts <code>_id</code> as an initial id value. if an <code>id</code> function<br />exists (further up the composition chain) it does not override it;<br />otherwise, it provides its own method for <code>id()</code></p></div>
<pre><code class="language-javascript">const identifiable = stampit()
.init(function(){
//accept id initializer value
let id = this._id
;(delete this._id)
if(!isFunction(this.id)) {
this.id = function() {
return (id || (id = cuid() ))
}
this.hasIdentity = () => {
return (typeof(id) !== 'undefined')
}
}
})</code></pre>
<section id="revisable">
<h1>revisable</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">declaration</div><span> </span><span>revisable</span><span> </span>
</p>
</section>
<div class="description"><p>encapsulates behaviors for revisioning of components</p></div>
<pre><code class="language-javascript">const revisable = stampit()
.init(function() {
let revision = 1</code></pre>
<section id="revision">
<h1>revision</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-info radius ctx-type">method</div><span> </span><span>this.revision()</span><span> </span>
</p>
</section>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:20%">Option name</th>
<th style="width:20%">Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>val</td>
<td>Number</td>
<td><p>the revision to set</p></td>
</tr>
</tbody>
</table>
<div class="description"><p>either get the current revision or set the revision with <code>val</code></p></div>
<pre><code class="language-javascript">this.revision = function (val) {
if(val) {
return (revision = val)
}
return revision
}</code></pre>
<section id="nextRevision">
<h1>nextRevision</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.nextRevision</span><span> </span>
</p>
</section>
<div class="description"><p>gets next revision (doesnt mutate state)</p></div>
<pre><code class="language-javascript">this.nextRevision = () => {
return (this.revision() + 1)
}
})</code></pre>
<section id="hashIdentityMap">
<h1>hashIdentityMap</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">declaration</div><span> </span><span>hashIdentityMap</span><span> </span>
</p>
</section>
<div class="description"><p>simple hashmap storage of event providers</p></div>
<pre><code class="language-javascript">const hashIdentityMap = stampit().init(function(){
let providers = new Map()
this.register = (id, provider) => {
if(!id) {
throw new Error('`id` is required')
}
if(!provider) {
throw new Error('`provider` is required')
}
providers.set(id, provider)
return provider
}
this.get = (id) => {
if(!id) {
throw new Error('`id` is required')
}
let provider = providers.get(id)
if(!provider) {
throw new Error('could not locate provider with id "' + id + '""')
}
return provider
}
this.release = () => {
providers.clear()
}
})
const nullStorage = stampit()
.init(function(){
this.store = () => { }
this.events = function*(from, to) {
return []
}
this.clear = () => { }
})
const inMemoryStorage = stampit()
.compose(revisable)
.init(function() {
var envelopes = []
this.store = (env) => {
if(!env.revision) {
env.revision = this.revision(this.nextRevision())
}
envelopes.push(env)
return this
}</code></pre>
<section id="clear">
<h1>clear</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.clear</span><span> </span>
</p>
</section>
<div class="description"><p>clear all envelops. DANGER ZONE!</p></div>
<pre><code class="language-javascript">this.clear = () => {
envelopes = []
}
this.events = function*(from, to) {
from = (from || 0)
to = (to || Number.MAX_VALUE)
if(from > to) {
throw new Error('`from` must be less than or equal `to`')
}
if(!envelopes.length) {
return []
}
for(let env of envelopes) {
if(env.revision > to) {
//we are done streaming
return
}
if(env.revision >= from) {
for(let ev of env.events) {
yield ev
}
}
}
}
})
const writeableUnitOfWork = stampit()
.refs({
storage: undefined
, identityMap: undefined
})
.methods({
envelope: function enveloper( events) {
return {
events: events
}
}
})
.init(function(){
let pending = []
this.append = (e) => {
pending.push.apply(pending,e)
return e
}
this.commit = () => {
//move reference to array in case event arrives while flushing
let committable = pending.splice(0,pending.length)
let envelope = this.envelope(committable)
this.storage.store(envelope)
return this
}
this.register = function() {
//no op
}
})
const readableUnitOfWork = stampit()
.refs({
storage: undefined
, identityMap: undefined
})
.init(function(){
this.append = (e) => {
//no op
return e
}
this.commit = () => {
return this
}
this.register = this.identityMap.register
//helper function to allow function binding during iteration
function asyncApply(event, identityMap) {
let target = identityMap.get(event.id)
return target.applyEvent(event)
}
const iterate = (cur, iterator, accumulator) => {
if(cur.done) {
return accumulator
}
let event = cur.value
let result = undefined
if(accumulator.promise) {
//chain promises
//effectively creating a complicated reduce statement
accumulator.promise = result = accumulator.promise
.then(asyncApply.bind(this, event, this.identityMap))
} else {
let target = this.identityMap.get(event.id)
let fn = target.applyEvent.bind(target, event)
try {
result = fn()
} catch(err) {
iterator.throw(err)
throw err
}
//was a promise returned?
if(result && result.then) {
accumulator.promise = result
}
}
return iterate(iterator.next(), iterator, accumulator)
}
this.restore = (root, from, to) => {
if(!root) {
throw new Error('`root` is required')
}
this.register(root.id(),root)
let events = this.storage.events(from, to)
let accumulator = {}
iterate(events.next(),events,accumulator)
if(accumulator.promise) {
return accumulator.promise
}
return this
}
})
const unitOfWork = stampit()
.refs({
storage: undefined
, identityMap: undefined
})
.methods({
envelope: function enveloper( events) {
return {
events: events
}
}
})
.init(function(){
let current
let writeable = writeableUnitOfWork({
envelope : this.envelope
, identityMap : this.identityMap
, storage : this.storage
})
let readable = readableUnitOfWork({
identityMap : this.identityMap
, storage : this.storage
})
this.append = (e) => {
let result = current.append(e)
if(!this.atomic) {
//each event gets stored
this.commit()
return result
}
return result
}
this.commit = () => {
current.commit()
this.identityMap.release()
return this
}
this.register = (id, provider) => {
return current.register(id, provider)
}
this.restore = (root, from, to) => {
current = readable
let result = current.restore(root, from, to)
if(result.then) {
return result
.bind(this)
.then(function(){
this.identityMap.release()
current = writeable
return this
})
} else {
this.identityMap.release()
current = writeable
return this
}
}
//by default we are in writeable state
current = writeable
})
const eventable = stampit()
.init(function(){
let uow = this.leo.unitOfWork()
//decorate event(s) with critical properties
let decorate = (arr) => {
let rev = this.nextRevision()
return arr.map((e) => {
e.id = this.id()
e.revision = rev++
return e
})
}
let assertIdentity = () => {
if(!this.hasIdentity()) {
throw new Error('identity is unknown')
}
}
let validateEvents = function (arr) {
for(let e of arr) {
if(!e || !e.event) {
throw new Error('`event` is required')
}
}
return arr
}
let pushEvent = (e) => {
assertIdentity()
if(!Array.isArray(e)) {
e = [e]
}
validateEvents(e)
decorate(e)
uow.append(e)
return e
}</code></pre>
<section id="raise">
<h1>raise</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.raise</span><span> </span>
</p>
</section>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:20%">Option name</th>
<th style="width:20%">Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>e</td>
<td>Object, Array</td>
<td><p>the event(s) to push</p></td>
</tr>
<tr>
<td>return</td>
<td>eventable</td>
<td><p>the result of <code>${event}</code> calls</p></td>
</tr>
</tbody>
</table>
<div class="description"><p>raise is the main interface you will use to mutate the eventable<br />instance and push the events onto the unit of work into storage.<br />The eventable instance revision is incremented.</p></div>
<pre><code class="language-javascript">this.raise = (e) => {
return this.applyEvent(pushEvent(e))
}</code></pre>
<section id="pushEvent">
<h1>pushEvent</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.pushEvent</span><span> </span>
</p>
</section>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:20%">Option name</th>
<th style="width:20%">Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>e</td>
<td>Object, Array</td>
<td><p>the event(s) to push</p></td>
</tr>
<tr>
<td>return</td>
<td>eventable</td>
<td><p>the stampit eventable instance</p></td>
</tr>
</tbody>
</table>
<div class="description"><p>pushEvent allows you to push event(s) directly into<br />the unit of work without mutating the provider.<br />this lets you stream into storage without getting into recursive loops</p></div>
<pre><code class="language-javascript">this.pushEvent = (e) => {
e = pushEvent(e)
this.revision(e[e.length - 1].revision)
return this
}
const applyEvent = (e, applied) => {
if(applied.length === e.length) {
return applied
}
let current = e[applied.length]
this.revision(current.revision)
applied.length = applied.length + 1
let fn = this['$' + current.event]
let result = undefined
if(applied.promise) {
if(!fn) {
return applyEvent(e, applied)
}
applied.promise = result = applied.promise
.return(current)
.bind(this)
.then(fn)
} else {
if(!fn) {
return applyEvent(e, applied)
}
result = fn.call(this, current)
//received a promise
if(result && result.then) {
applied.promise = result
}
}
applied.results.push(result)
return applyEvent(e, applied)
}
this.applyEvent = (e) => {
if(!Array.isArray(e)) {
e = [e]
}
let applied = {
results: []
, async: false
, length: 0
}
applyEvent(e,applied)
if(applied.promise) {
return Promise.all(applied.results)
}
return this
}
//register this instance on the unit of work
uow.register(this.id(), this)
})
export default stampit()
.static({</code></pre>
<section id="nullStorage">
<h1>nullStorage</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>nullStorage</span><span> </span>
</p>
</section>
<div class="description"><p>null object pattern for storage<br />when memory footprint is a concern or YAGNI storage<br />but want the benefits of event provider.<br />Handy for testing</p></div>
<pre><code class="language-javascript">nullStorage: nullStorage
})
.compose(identifiable)
.refs({</code></pre>
<section id="atomic">
<h1>atomic</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>atomic</span><span> </span>
</p>
</section>
<div class="description"><p><code>false</code> immediately stores events; otherwise, they are<br />queued to be committed to storage later.</p></div>
<pre><code class="language-javascript">atomic: true
})
.init(function() {
this.storage = (this.storage || inMemoryStorage())
this.identityMap = (this.identityMap || hashIdentityMap())
//default uow impl
let uow = unitOfWork({
storage: this.storage
, identityMap: this.identityMap
, atomic: this.atomic
})</code></pre>
<section id="eventable">
<h1>eventable</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.eventable</span><span> </span>
</p>
</section>
<div class="description"><p>Expose an <code>stamp</code> that may be use for composition<br />with another stamp</p></div>
<pre><code class="language-javascript">this.eventable = () => {
return stampit()
.props({leo: this})
.compose(identifiable)
.compose(revisable)
.compose(eventable)
}</code></pre>
<section id="commit">
<h1>commit</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.commit</span><span> </span>
</p>
</section>
<div class="description"><p>convenience method to commit pending events to storage</p></div>
<pre><code class="language-javascript">this.commit = () => {
return this.unitOfWork().commit()
}</code></pre>
<section id="unitOfWork">
<h1>unitOfWork</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.unitOfWork</span><span> </span>
</p>
</section>
<div class="description"><p>convenience method to unitOfWork inside <code>eventable</code> impl</p></div>
<pre><code class="language-javascript">this.unitOfWork = () => {
return uow
}</code></pre>
<section id="mount">
<h1>mount</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.mount</span><span> </span>
</p>
</section>
<div class="description"><p>mount an envelope having events into storage<br />useful for testing, or perhaps seeding an app from a backend</p></div>
<pre><code class="language-javascript">this.mount = (envelope) => {
this.storage.store(envelope)
return this
}</code></pre>
<section id="restore">
<h1>restore</h1>
<h5 class="subheader"></h5>
<p>
<div class="label label-success radius ctx-type">property</div><span> </span><span>this.restore</span><span> </span>
</p>
</section>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width:20%">Option name</th>
<th style="width:20%">Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>root</td>
<td>eventable</td>
<td><p>any <code>eventable</code> object</p></td>
</tr>
<tr>
<td>from</td>
<td>Number</td>
<td><p>lower bound revision to include</p></td>
</tr>
<tr>
<td>to</td>
<td>Number</td>
<td><p>upper bound revision to include</p></td>
</tr>
<tr>
<td>return</td>
<td>Promise</td>
<td><p>resolving this leo instance</p></td>
</tr>
</tbody>
</table>
<div class="description"><p>restore to revision <code>to</code> from revision <code>from</code><br />using <code>root</code> at the entrypoint. <code>to</code> and <code>from</code> are inclusive.</p></div>
<pre><code class="language-javascript">this.restore = (root, from, to) => {
return this.unitOfWork().restore(root, from, to)
}
this.revision = () => {
return this.storage.revision()
}
})</code></pre>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<p>Documentation generated with <a href="https://github.com/mr-doc/mr-doc">Mr. Doc </a> created by <a href="https://twitter.com/FGRibreau" data-show-count="false" class="twitter-follow-button">Francois-Guillaume Ribreau </a></p>
<p>Mr. Doc is sponsored by <a href="http://bringr.net/?btt" title="Outil d'analyse des réseaux sociaux" class="bringr">Bringr </a> and <a href="https://redsmin.com/?btt" title="Full Redis GUI" class="redsmin">Redsmin</a></p>
<p>Theme borrowed from Twitter Bootstrap</p>
</div>
</footer>
<script src="js/twitter-widget.min.js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/bootstrap-transition.min.js"></script>
<script src="js/bootstrap-scrollspy.min.js"></script>
<script src="js/bootstrap-dropdown.min.js"></script>
<script src="js/bootstrap-collapse.min.js"></script>
<script src="js/bootstrap-affix.min.js"></script>
<script src="js/prism.min.js"></script>
<script src="js/index.min.js"></script>
</body>
</html>