FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

LiveReload/node_modules/newreactive at develop · livereload/LiveReload · GitHub

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Reactive Models for JavaScript

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:

  • allows you to define models, collections and their attributes;
  • provides type-checking, type coercion, default values and other goodies for attributes;
  • allows you to split model classes into several mixins;
  • defines creation and disposal behavior of the models.

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.

Getting Started

Installation

Clone this repo; not yet published on npm.

Hello World Example

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)

Status Bar Example (CoffeeScript)

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:

  • models are plain JavaScript classes registered via universe.define;
  • those JavaScript classes are actually mixins; multiple mixins may be combined into a single model class (e.g. App_Basics and App_Handlers mixins are combined into App); mixins follow ModelName_MixinName naming convention;
  • attributes must be explicitly defined in a schema if you want Reactive.js to know about them (of course, you can still use regular JavaScript fields like authors in our example; those are simply be ignored by the framework)
  • a method named computed_smt must correspond to a schema-defined attribute and is invoked to set the value of the attribute; any attributes accessed by the method are tracked as dependencies, and the method is re-invoked when its dependencies are updated;
  • a method named automatically_smt is invoked shortly after the model is created; any attributes accessed by the method are tracked as dependencies, and the method is re-invoked when its dependencies are updated.

Concepts

Reactivity

TODO: explain more, although the idea should be pretty clear by now.

Entities and Mixins

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:

  • attribute definitions (and everything else that might be defined by a schema in the future)
  • class prototype members
  • automatic blocks (automatically_do_something)
  • initialize hooks (initialize(attrs)), other hooks in the future

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.

Attributes and Schemas

Types

Valid type descriptors

Simple types:

  • undefined or 'any' — any value, defaults to undefined
  • 'int' — integer value, defaults to zero
  • 'number' or Number — numeric value, defaults to zero
  • 'string' or String — string value, defaults to an empty string
  • MyClass or { object: MyClass } — instance of a specified class, has no default value

Compound types:

  • 'array' or Array — array of any values, defaults to an empty array
  • { array: itemType } — array of values conforming to itemType descriptor, defaults to an empty array
  • [type1, type2, type3] — “or”: value conforming to one of the specified types, defaults to type1's default value

Reactive library types:

  • 'SomeEntity' — registered entity
  • 'SomeDuckType' — registered duck type

Nullability:

  • the only types that accept null values are 'any' and null
  • all other types, for instance object and entity types, do not accept nulls
  • use null type with an “or” descriptor to allow nulls, e.g. [null, 'string'] or { array: [null, 'int'] }
  • alternatively, for descriptors specified as a string, append ? to the string to allow nulls, e.g. 'string?', 'MyEntity?', { array: 'int?' }

API

R.Universe

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.

new R.Universe()

Creates a new empty universe; takes no arguments.

universe = new R.Universe()

universe.define(arg1, [arg2, arg3], arg4)

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.

Defining a mixin

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:

  • all prototype attributes and methods are copied into the entity class prototype;
  • all static attributes and methods (i.e. those defined on the function itself) are copied into the entity class.

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:

  • mixinClass.prototype.initialize(attributes) is invoked when a new model instance is created;
  • mixinClass.prototype.schema or mixinClass.schema is an object that defines attributes and other metadata of the entity (see below).

Defining a duck type

universe.define({ name: 'Foo', duck: { key: value } })

TODO.

universe.create(kindName, attributes)

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.

universe.performAndWait(func, callback)

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.

universe._r_id

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).

universe.dispose()

Destroys the universe, cancels any universe-related activity that can be cancelled.

R.Entity

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:

  • you do not derive from R.Entity; write mixin classes and register them via universe.define instead;
  • you do not instantiate R.Entity or its subclasses directly; use universe.create instead.

entity.isReactiveEntity

Always true; use this for duck-type instance-of checks. Do if (entity.isReactiveEntity), don't do if (entity instanceof R.Entity).

entity.universe

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.

entity . attr

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.

entity.get(attr), entity.set(attr, value)

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.

entity.isKindOrSubclass(kind)

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.)

entity._r_id

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).

Mixins, Lifecycle and Magic Methods

A mixin can define the following special methods which are not copied into the entity class.

MyMixin.prototype.automatically_smt

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}"

Attributes and Schemas

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.

Schema key: $extends

Specifies a list of entity kinds to inherit from. Must be a string or a list of strings.

Schema key: $includes

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).

Schema key: $mixins

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.

Magic methods

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:

  • get_foo — foo's getter
  • set_foo — foo's setter
  • compute_foo_ — foo's compute
  • initialize_foo — foo's initializer

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.

Attribute metadata: type (not implemented yet)

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'].

Attribute metadata: default

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).

Attribute metadata: compute

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.

Attribute metadata: getter

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.

Attribute metadata: setter

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.

Attribute metadata: initializer (not implemented yet)

Can be specified as initialize_attrName magic method. Defaults to null.

Attribute metadata: reactive

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:

  • to define a virtual property (Object.defineProperty) with a given getter and setter (note that Reactive.js won't copy properties from the mixin prototypes, so this is the only way to define a virtual property)
  • to allow reading/writing a field via get() and set() methods
  • (not implemented yet) to make the attribute visible to the automatic dependency injection system (note that dependency injection for reactive attributes only exists in my head so far)
  • just to have it listed in the schema, as a sort of a fucked way to declare your public API
  • (not implemented yet) to provide a default value inside the schema (note that a simple assignment in a constructor would do just fine for that)
  • (not implemented yet) to enable type-checking and coercion of assigned values (in the future versions, Reactive.js will define accessor methods to automatically perform checking/coercion if you specify the attribute type)

Collections

Example collection attribute:

systemRubies: { collection: 'list', type: 'LRRuby' }

Collection attribute metadata: collection (required)

Specifies a collection class; this must either be one of the predefined string values, or a JavaScript function.

Predefined collection classes:

  • list specifies R.ListCollection.

Collection attribute metadata: type (required)

Specifies the element type of the collection. Must be one of the defined entity kinds (we don't support non-entity collections yet).

Private API

Tasks

Generally, a task is a separately scheduled unit of work. Reactive.js uses tasks for 4 purposes:

  • to provide a way to schedule and prioritize the background work (like recomputation of computed properties);
  • to track completion of the given work with all its side effects;
  • to establish reactive environment boundaries and associate dependencies metadata with reactive code;
  • to handle errors in an organized way.

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:

  • One-shot tasks are only ever executed once; they are primarily used to track completion of initialization operations.
  • Multi-shot task can be invoked multiple times (manually), although Reactive.js guarantees that if a task is already running, further invocations are postponed until the current invocation completes.
  • Autorepeat tasks are multi-shot tasks that track any attributes accessed during their execution, and are automaitcally reinvoked when those dependencies are updated.

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.

Task invocations

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.

Completion, finalization and subtasks

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.

Tasks API

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