lain v2.1.2
Lain.js
Lain is an in-memory data store whose structure is composed at run-time. It is most suitable for applications that are composed incrementally with extremely loose-coupling and many external modules.
It acts as tree of observable scopes. Instead of relying on functional or lexical scopes inherent to a programming language, Lain lets you create a scope hierarchy on the fly. This allows the simple creation of component or module-based scopes.
Lain is primarily designed to be used as a library for other libraries (as it lacks data-flow or data-binding mechanisms other than pub/sub).
Usage
First, let's create some scopes (imagining that they correspond to components on a web page).
var appScope = Lain.createChild(); // creates a top-level scope
var pageScope = appScope.createChild();
var menuScope = pageScope.createChild();
var buttonScope = pageScope.createChild();
Each of these scopes can hold named instances of the Data
class.
var country = appScope.data('country'); // returns an empty instance of class Data
country.write('Japan'); // stores the string 'Japan'
Grab a reference to a Data
instance in the current scope with the grab
method.
var sameCountry = appScope.grab('country'); // returns the Data instance containing 'Japan'
Data
defined in higher scopes can be accessed using Scope.find
.
buttonScope.find('country').read(); // returns 'Japan'
Scopes can override Data
instances, blocking access to Data
of the same name in higher scopes, as is typical in most programming languages.
pageScope.data('country').write('Russia');
appScope.data('country').write('Argentina');
buttonScope.find('country').read(); // returns 'Russia' since pageScope is found before the appScope
buttonScope.find('country').write('France'); // can overwrite the stored value
The default Data instance allows read and write access from its local and descendant scopes.
buttonScope.find('country').write('France'); // can overwrite the stored value
menuScope.find('country').read(); // returns 'France' now when accessed from a sister scope
Additionally, scopes can specify that variables (represented via the Data
class) act as states or actions.
appScope.action('navigate');
var url = pageScope.state('url');
pageScope.find('navigate').subscribe(function(msg){
url.write(msg + '.html');
});
States can only be updated in their local scope and are read-only from lower scopes.
pageScope.find('url').write('cat.html'); // updates successfully
menuScope.find('url').write('cat.html'); // throws an Error since it is read-only from the child scope
Actions can be updated such that they emit their values, but they do not retain them and are thus stateless.
pageScope.find('navigate').write('bunny'); // updates subscribers
menuScope.find('url').read(); // returns 'cat.html'
// peek() returns the last Packet instance
// {msg: 'cat.html', topic: null, source: 'url', timestamp: Date.now()}
menuScope.find('url').peek();
menuScope.find('navigate').read(); // returns `undefined`
menuScope.find('navigate').peek(); // returns `null`
Every Data instance can treated as a discrete value or as a full pub/sub channel with subscriptions or values available by topic (via the subscribe
method).
var fields = appScope.data('fields');
fields.write('three fields here'); // stored on the default `null` topic
fields.write('bunny', 'animal'); // 'bunny' stored on the 'animal' topic
fields.write('grass', 'food'); // 'bunny' stored on the 'food' topic
fields.subscribe('animal', function(msg, packet){
console.log(msg, packet.topic);
};
// the callback is not invoked until something new is written
fields.write('elephant', 'animal'); // writes to the console now
The follow
method acts just like subscribe
but will also emit the current state of the Data
instance (if present) when invoked.
// the callback here is invoked immediately with the stored value and the last stored Packet
fields.follow('animal', function(msg, packet){
console.log(msg, packet.topic);
};
Updates across all topics (essential for debugging this pattern) can be accessed using the monitor
method (basically a wildcard subscription).
fields.monitor(function(msg, packet){
console.log(packet.topic + ':' + msg);
};
fields.write('cat', 'animal'); // logs: 'animal:cat'
fields.write('mice', 'food'); // logs: 'food:mice'
fields.write('house'); // logs: 'null:house'
To sandbox its descendant scopes, a scope can declare a white-list of available variable names (referred to as valves). Valves allow subscriptions to be mediated through 'inversion of access' (where encapsulation is declared from above).
appScope.data('color').write('red');
appScope.data('shadow').write('blue');
appScope.data('mixture').write('purple');
pageScope.valves(['color','shadow'); // allows access to only 'color' and 'shadow' `Data` from lower scopes
pageScope.find('color'); // returns the `Data` instance containing 'red'
pageScope.find('mixture'); // returns the `Data` instance containing 'purple'
buttonScope.find('color'); // returns the `Data` instance containing 'red'
buttonScope.find('mixture'); // returns null due to the valves in pageScope
To create parallel hierarchies of data with the same inherent structure but different access properties (useful for separating things like source file information, styles, api methods, etc.), scopes can declare that Data elements reside in a specific dimension (like a namespace of sorts). Valves can be defined separately for each dimension.
// this returns an appScope instance that accesses data stored in a 'style' namespace
var styles = appScope.dimension('style');
styles.data('shadow').write('blue');
buttonScope.find('shadow'); // returns null
buttonScope.dimension('style').find('shadow'); // returns the `Data` instance containing 'blue'
Valves can be configured separately for each dimension, allowing flexible white-listing.
// this returns an appScope instance that accesses data stored in a 'style' namespace
var styles = appScope.dimension('style');
styles.data('background').write('red');
styles.data('shadow').write('blue');
styles.data('mixture').write('purple');
buttonScope.find('shadow'); // returns null
buttonScope.dimension('style').find('shadow'); // returns the `Data` instance containing 'blue'
pageScope.valves(['color','shadow'); // allows access to only 'color' and 'shadow' `Data` from lower scopes
pageScope.find('color'); // returns the `Data` instance containing 'red'
pageScope.find('mixture'); // returns the `Data` instance containing 'purple'
buttonScope.find('color'); // returns the `Data` instance containing 'red'
buttonScope.find('mixture'); // returns null due to the valves in pageScope
Installation
Install the module with: npm install lain
or place into your package.json
and run npm install
.
Documentation
Class: Scope
Create a Scope
from Lain (which is the root Scope
) or another Scope
using the createChild
method.
Methods
createChild([name])
Create a new child scope. The name property is just cosmetic (for debugging).children()
Returns an array (shallow copy) of child scopes.clear()
Destroys all elements and children within the scope, effectively resetting it.destroy()
Destroys the scope and everything within it.data(name, [dimension])
Gets or creates a localData
instance with the given name.action(name, [dimension])
Gets or creates a localData
instance configured as an action. It is stateless and will emit but not store values.state(name, [dimension])
Gets or creates a localData
instance configured as a state. It is read-only when accessed from any child scope.grab(name, [dimension])
Returns a localData
instance (data, state or action) ornull
if not present.find(name, [dimension])
Searches for aData
instance in the current scope and then continues searching up the scope tree. Returnsnull
if no matches are found or if the search is blocked by a valve.multiWrite(writes, [dimension])
Finds and writes to multipleData
instances at once, notifying subscribers only after all writes have been completed. Thewrites
argument can be either a hash of names and values or an array of{name: n, value: v, topic: t}
objects.kills(destructible, [destructor])
Ties the lifecycle of a destructible object to the scope. When the scope is destroyed or cleared, the destructible's destroy (or dispose) method will be called. An alternate destructor method can be specified as well.insertParent(scope)
Inserts a scope between this scope and its parent scope.setParent(scope)
Assigns a parent scope to the current scope, removing its original parent (if any). Scopes can be orphan via setParent(null).flatten([dimension])
Creates a hash of allData
instances accessible to the current scope.localDataSet([dimension])
Creates a hash of allData
instances in the current scope. (NOT YET IMPLEMENTED)findDataSet(names, [dimension])
Creates a hash ofData
instances found through the current scope using the given names.readDataSet(names, [dimension])
Like findDataSet -- but returns the message values instead of theData
instances.dimension([name])
Returns the current scope with a wrapper accessing the specified dimension as its default.
Class: Data
The Data
class is a generic pub/sub box for holding data values by topic. If no topic is specified, the values are stored with a null
topic.
Each topic maintains a list of listeners/subscribers that can receive updates when new values are received.
Methods
read([topic])
Returns the lastmsg
written to the topic (orundefined
if never used).write(msg, [topic])
Write amsg
value to be stored (on the topic if specified). This will immediately notify any subscribers.toggle([topic])
Toggles the booleanmsg
value of the instance (writing!msg
).refresh([topic])
Notifies all subscribers of the currentmsg
value.subscribe(watcher, [topic])
Subscribes to theData
instance.watcher
can be a function or object with atell
method likefunction(msg, packet)
.follow(watcher, [topic])
Subscribes and immediately emits the currentmsg
andpacket
values if present.monitor(watcher)
Subscribes to all topics on theData
instance (including topics added later).name()
Returns the instance namedimension()
Returns the dimension (defaults to 'data')dead()
Returns true if the instance has been destroyed.destroy()
Removes and destroys the instance and its subscriptions
Class: Packet
All writes made against Data
instances are stored and/or emitted as both a Packet
envelope and the original value of the write message
itself (the 'msg' property in the Packet
). The full Packet
can be inspected using the Data.peek
method. It is also sent as the second argument
of all subscription callbacks.
Properties
msg
Message content written to aData
instance.topic
Subscription topic that created the packet (the default isnull
).source
Name of theData
instance that stored or emitted the packet.timestamp
Date.now() when created.
License
Copyright (c) 2016 Scott Southworth & Contributors Licensed under the Apache 2.0 license.