| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
parent directory.. | ||||
Reactive.js is a reactive enviroment and a modelling library for desktop apps written in JavaScript.
As a reactive environment, Reactive.js provides automatic dependency tracking for your models and view models / controllers, eliminating the need to emit and subscribe to change events:
If you render a status bar using mainScreen.statusText value, which is computed based on app.connectionCount value and app.messages collection, then whenever one of those source values are updated, statusText value is automatically re-computed, and then the status bar rendering code is automatically re-invoked.
As a modelling library, Reactive.js:
As a library aimed for desktop and desktop-style apps, Reactive.js is completely oblivious to any client/server data transfer issues, which, in its present state, probably makes it unsuitable for most web apps.
Reactive.js does not currently provide any data-binding facilities; those can be developed on top of it.
State: pre-alpha.
Clone this repo; not yet published on npm.
A model that automatically outputs a greeting every time the name is updated:
function App() {}
App.schema = {
name: { type: String }
};
App.prototype.automatically_say_hello = function() {
console.log("Hello, " + this.name + "!");
};
To use it, we need to create a universe and register the model class first:
var universe = new R.Universe(); universe.define(App);
Then we can create and use the model:
var app = universe.create('App', { name: 'world' });
setTimeout(function() {
app.name = "stranger";
}, 1000);
Outputs:
Hello, world! Hello, stranger! (after 1 sec)
A console app that re-renders its status bar whenever the connection count or the message list is updated (which is simulated every couple of seconds):
class App_Basics
schema:
mainScreen: { type: 'MainScreen' }
connectionCount: { type: 'int' }
messages: { collection: 'list', type: 'Message' }
initialize: ->
@mainScreen = @universe.create('MainScreen')
class App_Handlers
initialize: ->
@authors = ['Paul', 'Bob', 'Rick', 'Mike', 'Jeff']
clientConnected: ->
@connectionCount += 1
messageReceived: ->
author = @authors.shift(); @authors.push(author)
@messages.push @universe.create('Message', author: author, text: "Hello!")
class Message
schema:
author: { type: String }
text: { type: String }
class MainScreen
schema:
app: { type: 'App', injected: yes }
statusText: { type: String }
compute_statusText: ->
lastMessage = @app.messages.last()
"#{app.connectionCount} connections, last message from #{lastMessage?.author or 'nobody'}"
automatically_render_statusText: ->
console.log "statusText = #{@statusText}"
exports.run = ->
universe = new R.Universe()
universe.define(App_Basics, App_Handlers, Message, MainScreen)
app = universe.create('App')
setInterval app.clientConnected.bind(app), 1500
setInterval app.messageReceived.bind(app), 2500
Outputs:
0 connections, last message from nobody 1 connections, last message from nobody (after 1.5 sec) 1 connections, last message from Paul (after 2.5 sec) 2 connections, last message from Paul (after 3.0 sec) 3 connections, last message from Paul (after 4.5 sec) 3 connections, last message from Bob (after 5.0 sec) ...
Key facts to observe here:
TODO: explain more, although the idea should be pretty clear by now.
TODO.
Reactive.js supports multiple inheritance of entities, which works the way you'd expect (using Python-style DFS ordering for conflicting members). The list of superclasses is specified via $extends key of a schema:
class Rule
...
class FileToFileRule
schema:
$extends: 'Rule' # or ['Rule', 'SomeOtherEntity']
Note that you specify entity kind names, not mixin names/classes.
Entity inheritance is used for type checking and to share implementations. The following implementation details are inherited:
One could say that a child entity inherits the parent entity's mixins. There's a twist, however: sibling mixins cannot override each others' attributes and methods, but the members defined in a child entity do override the inherited members.
There's no easy way to call super implementation from an overridden method, sorry. Once I encounter a use case for that, I'll figure something out.
Simple types:
Compound types:
Reactive library types:
Nullability:
A top-level context for reactive models.
Normally, an app only has a single instance of Universe, and when running tests, every test has its own universe.
Creates a new empty universe; takes no arguments.
universe = new R.Universe()
Adds the given definitions to the universe. Define recursively processes its arguments and any arrays given as arguments (and arrays within arrays, etc). The individual definitions must conform to the syntaxes given below.
universe.define(mixinClass)
universe.define({ klass: mixinClass })
Mixes the given mixinClass (which must be a JavaScript function with a prototype) into a similarly-named entity kind, creating the kind if it does not exist.
Specifically:
The entity kind name and the mixin name are derived from the name of the given function. For that, mixinClass.name must be defined and non-empty (which happens automatically with most JavaScript engines). If mixinClass.name contains an underscore (Foo_Bar), the first part is used as an entity kind name, and the second part as a mixin name. If mixinClass.name does not contain an underscore, it is used as an entity kind name, and the mixin name is empty.
Note that the given function is never invoked, and thus mixinClass is never instantiated; it is only used for its members. Any prototype methods are always invoked with the actual entity in this.
The following keys are treated specially and are not copied into the entity class:
universe.define({ name: 'Foo', duck: { key: value } })
TODO.
Creates a new entity of the given kind, initialized with the given attribute values. Returns an instance of an entity class defined by the reactive library, which derives from R.Entity and has all defined mixins applied.
kindName must be a string identifying a known entity kind.
attributes is an optional hash. Keys matching the names of defined attributes will be assigned to those attributes. All other keys can be handled inside initialize methods of mixin classes.
Performs the given function and waits until all side effects are processed. Most useful in tests that want to check a computed attribute after updating a source one.
Works by scheduling the given function as a ONESHOT task, then addeding the given callback as a finalization handler of the task invocation.
In all classes defined by the reactive library, _r_id is a unique immutable string value that can be used to identify a given object from elsewhere (especially useful as a dictionary key).
Destroys the universe, cancels any universe-related activity that can be cancelled.
A base class for all reactive models.
Keep in mind: while the (automatically defined) model classes derive from R.Entity, your mixin classes do not, so you should not reference this class in your code:
Always true; use this for duck-type instance-of checks. Do if (entity.isReactiveEntity), don't do if (entity instanceof R.Entity).
Every entity class is bound to a specific instance of Universe, which can be accessed via EntityClass.prototype.universe and via entity.universe. This is most often used to invoke this.universe.create(...) from within the entity.
Attributes defined by a schema are directly exposed on an Entity. E.g. if your schema defines attribute foo, you can access it as entity.foo and entity.foo = 42.
Backbone-compatible get/set methods accepting a string attr name. They invoke individual attribute getters/setters, so should behave exactly the same way.
Unlike in Backbone, you cannot get/set arbitrary attributes; every attribute accessed by these methods must be defined in the entity's schema.
Returns true if the entity is of the specified kind, false otherwise. (When entity inheritance is added, will also return true when the entity is a subclass of a given kind.)
In all classes defined by the reactive library, _r_id is a unique immutable string value that can be used to identify a given object from elsewhere (especially useful as a dictionary key).
A mixin can define the following special methods which are not copied into the entity class.
Any method that starts with automatically_ is invoked when the model is instantiated, inside a reactive task; it will be invoked again when the values of any accessed attributes are updated.
For example, this class continuously logs the current value of SimpleEntity.someValue to console:
class SimpleEntity
schema:
someValue: { type: 'int' }
automatically_log_someValue: ->
console.log "@someValue = #{@someValue}"
A schema is a JavaScript object specified as MixinClass.schema or MixinClass.prototype.schema. (The latter version is a convenience for CoffeeScript users; putting the schema into the prototype does not make much sense otherwise.)
Every key of the schema is an attribute name, and the value defines the attribute's metadata. Keys starting with a dollar sign are special and do not correspond to attributes. E.g.:
class Message
schema:
$mixins: [Bar, Boz]
name: { type: String }
author: { type: [null, 'Person'] }
demo: {}
An attribute metadata with a collection key defines a collection, which adds more keys and alters the meanings of some other keys; see the dedicated section on collections.
Specifies a list of entity kinds to inherit from. Must be a string or a list of strings.
Specifies a list of mixins to include into the current mixin's entity. Must be a class or a list of classes; the classes are something that you could pass to universe.define, short for the naming convention (you don't have to prefix included mixins with the entity name; you can have shared mixins that are included into multiple entities).
Not implemented yet, and might never be. :-)
Specifies a list of mixins to define when this mixin is defined; must be an array of JavaScript functions that can be passed to universe.define.
Some attribute metadata keys can be provided as mixin prototype methods. E.g. a method named compute_foo specifies a function for the compute key of the foo attribute's metadata.
The following magic methods are defined:
To avoid typos, any method starting with one of the magic names must correspond to an existing attribute, otherwise an error is thrown at mixin registration time.
Limits the attribute value to the specified types; if null is not one of the types, the attribute must be initialized to a non-null value.
Defaults to [null, 'any'].
Specifies the initial value for the attribute. This value will be used when creating new entities unless another value is provided to the constructor (in the attributes dictionary).
schema:
eventCount: { type: 'int', default: 42 }
Defaults to the appropriate value for the attribute type (null for null, 0 for int, empty string for String).
Specifies a function that provides a value for this attribute. The function will be called shortly after the model is created. The call is made inside a reactive task, so any accessed attributes will be tracked as dependencies, and the call is repeated when the dependencies are updated. The value of this is the model instance.
The function must accept zero or one arguments. A zero-argument compute function must be synchronous and must return the new value. A one-argument function is asynchronous and is invoked as compute(callback); the return value is ignored, and the callback must be eventually called as callback(err, newValue). Reactive.js guards against re-entrance; if the function is already running, it will not be called again until the callback is invoked.
Can (and probably should) be specified as compute_attrName magic method. Defaults to null.
A method to call (synchronously) to read the value of the attribute. This key is currently only supported for non-reactive attributes (reactive: false).
Can be specified as get_attrName magic method.
A method to call (synchronously) to update the value of the attribute. This key is currently only supported for non-reactive attributes (reactive: false).
Can be specified as set_attrName magic method.
Can be specified as initialize_attrName magic method. Defaults to null.
When set to false, turns off reactive behavior for this attribute. If a getter/setter is specified, Object.defineProperty will be used to define this attribute on the model class prototype. If there's no getter and no setter, there will be no property accessor defined, so reads and writes will be accessing the actual JavaScript field.
Here's a full list of reasons why you might want to define a non-reactive attribute:
Example collection attribute:
systemRubies: { collection: 'list', type: 'LRRuby' }
Specifies a collection class; this must either be one of the predefined string values, or a JavaScript function.
Predefined collection classes:
Specifies the element type of the collection. Must be one of the defined entity kinds (we don't support non-entity collections yet).
Generally, a task is a separately scheduled unit of work. Reactive.js uses tasks for 4 purposes:
Reactive.js provides a simple task queue, but the queueing aspects are not the primary focus of the framework. In the future we anticipate an API to integrate Reactive.js tasks with more sophisticated queuing solutions.
Reactive.js defines ONESHOT, MULTISHOT and AUTOREPEAT task types:
Tasks can be synchronous and asyncronous. Reactive.js uses Node.js domains to make any asyncronous callbacks part of their originating task, and to treat any exceptions thrown by those callbacks as a failure of the task.
A task belongs to a given entity, which is used to automatically dispose tasks (especially autorepeat tasks) when their owner entity is disposed.
Tasks have invocations; at any given time, a task has zero or one scheduled invocations, zero or one running invocations, and zero or one completed invocations. (Of course, a one-shot task can only have a single invocation ever; it will refuse to schedule an invocation if another one is already running or completed.)
Dependencies are tracked by invocations; autorepeat tasks subscribe to the most recently completed invocation's dependencies.
A task invocation is completed when the task's code and all its asynchronous work has completed (succeeded or failed).
A task invocation is finalized when it has completed, and any side-effects (e.g. computed properties recomputation) scheduled during its execution have completed too.
Completion is typically used when a task needs to invoke another task and wait for it to complete before doing further work, while finalization is typically used by the tests to check the side effects of a specific operation.
Completion and finalization is tracked by task invocations; when you schedule a task, you get a scheduled invocation back which you can subscribe to (invocation.waitCompleted and invocation.waitFinalized).
If task A is scheduled while another task B is running, the scheduled invocation of task A becomes a child of the running invocation of task B. Parent-child relationships are used to track finalization: invocation A is finalized iff it is completed and all its child invocations are finalized.
The raw tasks API is typically hidden behind more convenient APIs like compute_smt or automatically_smt magic methods. We intend to keep the Tasks API for private use and expose use case-specific wrappers instead.
For now, though, you might be forced to deal with the raw API, particularly in tests. It is a bit of a mess, and the API is subject to change at any time.
You'll be dealing with 3 objects: R.TaskDef (a definition that can be shared by multiple entities), R.Task (an actual task that binds R.TaskDef instance to a given entity) and R.TaskInvocation.
To speed up creation of many similar tasks, pretty much everything about a task needs to be described as a R.TaskDef first. Right now this merely saves a bit of memory, but in the future, some heavier processing may be performed with TaskDefs (like maybe determining a priority).
R.TaskDef accepts a function that must accept 0, 1 or 2 arguments. If it accepts 2 arguments, it is called as func(entity, callback) and the task is asynchronous. Otherwise, it is called as func(entity) and the task is synchronous. The entity is also used as this value for the func, so if the function is a method of an entity, you don't need to bind it.
Here's how you can create a one-off task and wait for its finalization:
class Dummy universe.define(Dummy) dummy = universe.create(Dummy) task = new R.Task dummy, new R.TaskDef universe, "Dummy", R.TaskDef.ONESHOT, (entity) -> console.log "I'm a task, and I am running" task.schedule().waitFinalized -> console.log "My invocation is completed, I could check side effects here!"
Here's how an entity can create a multishot task:
class MainWindow
schema:
someAttr: { type: 'int', default: 1 }
anotherAttr: { type: 'int' }
initialize:
# share a single TaskDef among all class instances
@constructor.buttonTaskDef or= new R.TaskDef(@universe, "Dummy", R.TaskDef.ONESHOT, @handleButton)
@buttonTask = new R.Task(this, @constructor.buttonTaskDef)
setInterval =>
@buttonTask.schedule().waitFinalized =>
console.log "Now all side effects caused by @someAttr++ in handleButton() have been processed"
console.log "In particular, now @someAttr == #{@someAttr} and @anotherAttr == #{@anotherAttr}"
, 1000
handleButton: ->
console.log "Button pressed!"
@someAttr++
console.log "Note that @anotherAttr == #{@anotherAttr}, but @someAttr == #{@someAttr}"
compute_anotherAttr: ->
@someAttr * 100
| Back | FazBrowse Home | New Git URL |