GithubHelp home page GithubHelp logo

salt's Introduction

Salt

State And Logic Traversal, for today's infinite-application.

by Bemi Faison

Build Status Gitter

Description

Salt is a state-based JavaScript library that offers unparalleled code organization and unprecedented flow control. No matter how you write code, the event loop sees it as a sequence of functions, invoked over time. Salt provides a simple pattern to describe those sequences, along with an API to control timing. The result is an async-agnostic flow of your functionality, in code, bridging the gap between specification and implementation.

Salt is designed to produce long-lived documents that can grow with your requirements. Traditional, bottom-up designs fail this test of architecture, because change inevitably destabilizes dependency models. Instead, Salt uses an autonomy model that groups isolated logic, and accommodates change with increasingly granular states. This top-down approach provides a holistic view of your application's functional relationships, along with the freedom and confidence to change them.

Usage

Use Salt to define and control your "program". Programs use structure to abstract functionality into state and logic; each lexical scope forms a state, and logic is declared via "tags" - i.e., keys prefixed by an underscore ("_"). Tags declare everything, from state behavior, access control rules, and - of course - bind callbacks to transitions.

Defining your Program

Begin by passing your program to a new Salt instance. The "hello world!" example below features a contrived number of states, in order to demonstrate additional features.

var helloSalt = new Salt({
  _data: 'phrase',
  piece: {
    _in: function () {
      this.data.phrase = 'hello';
    },
    together: function () {
      this.data.phrase += ' world!';
    },
    _tail: '@next'
  },
  speak: function () {
    console.log(this.data.phrase);
  },
  _out: function () {
    console.log('good-bye!');
  }
});

Internally, Salt compiles this into a private state-chart, where the following endpoints may now be traversed.

  1. "..//" at index 0
  2. "//" at index 1
  3. "//piece/" at index 2
  4. "//piece/together/" at index 3
  5. "//speak/" at index 4

The first two states exist in every Salt instance: the null and program-root states, respectively. As well, all instances start "on" the null state, which parents the program-root - the first state from your program. The state index reflects it's compilation order, and can be used to reference navigation targets.

Using state order in logic

While tag order is irrelevant, state order can matter in a program. Understandably, some developers are uncomfortable treating an object like an array. Thus, despite exhibiting the same FIFO object-member behavior, wary developers can rest assured that you Salt may be used with it's order-dependent features. Read more about key order preservation in the wiki.

For instance, with the example above, you could replace the spatial query "@next" with the relative query "../speak". The relative query requires a specific sibling; a state named "speak. The spatial query requires a specific relationship; the older/right-adjacent sibling state. Both retain a level of portability, but only one relies on sibling order.

Controlling your Salt instance

In order to execute your logic, direct your Salt instance toward a program state - i.e., one of the pre-compiled endpoints. Salt then observes the logic of states along it's navigation path, which can invoke functions based on how a state is traversed (or, the transition type). Navigation ends when the destination state has completed an "on" transition.

The example below uses the .go() method to direct our Salt instance towards the "//piece/together/" state.

helloSalt.go('//piece/together');
// hello world!

You can also inspect the .state property, in order to determine where your Salt instance is in your program.

console.log(helloSalt.state.path);  // "//speak/"
console.log(helloSalt.state.name);  // "speak"
console.log(helloSalt.state.index); // 4

To "exit" a program, direct Salt toward it's null state (at index 0). The null state parents the program root, allowing you to trigger any program entry/exit logic.

helloSalt.go(0);
// good-bye!

This program also uses the _data tag to define "scoped" data properties. While the .data.phrase property is used, it's no longer available when on the null state. This is because the Salt instance exited the state which declared the scoped property.

console.log(helloSalt.state.index); // 0
console.log(helloSalt.data.phrase); // undefined

Salt features a hybrid, declarative and procedural architecture, which offers many programmatic advantages and opportunities... it's extensible to boot! Please visit the online wiki to review the instance API and program tag glossary, for information on how to best use Salt in your code.

Installation

Salt works within modern JavaScript environments, including CommonJS (Node.js) and AMD (require.js). If it isn't compatible with your favorite runtime, please file a bug, or (better still) make a pull-request.

Dependencies

Salt depends on the Panzer library. Visit the Panzer project page, to learn about it's dependencies and requirements.

Salt also uses the following ECMAScript 5 features:

You will need to implement shims for these methods in unsupported environments - specifically , Internet Explorer versions 6, 7 & 8. (Augment.js shims these and other missing methods.)

Web Browsers

Use a <SCRIPT> tag to load the salt.min.js file in your web page. The file includes Salt dependencies for your convenience. Loading this file adds the Salt namespace to the global scope.

  <script type="text/javascript" src="path/to/salt.min.js"></script>
  <script type="text/javascript">
    // ... Salt dependent code ...
  </script>

Node.js

  • npm install salt if you're using npm
  • component install bemson/salt if you're using component
  • bower install salt if you're using Bower

AMD

Assuming you have a require.js compatible loader, configure an alias for the Salt module (the alias "salt" is recommended, for consistency). The salt module exports the Salt constructor, not a module namespace.

require.config({
  paths: {
    salt: 'libs/salt'
  }
});

Note: You will need to define additional aliases, in order to load Salt dependencies.

Then require and use the module in your application code:

require(['salt'], function (Salt) {
  // ... Salt dependent code ...
});

Warning: Do not load the minified file via AMD, since it includes Salt dependencies which themselves export modules. Use AMD optimizers like r.js in order to roll-up your dependency tree.

Testing

Salt has over 350 unit tests to inspire and guide your usage. They were written with Mocha, using Chai and Sinon (via the Sinon-chai plugin).

  • To browse test results, visit Salt on Travis-CI.
  • To run tests in Node, invoke npm test
  • To run tests in a browser: (1) install Salt, then (2) load test/index.html locally. (Unfortunately, the tests do not run in IE6, 7, or 8.)

Shout-outs

  • Peter Jones - Best. Sounding. Board. Ever.
  • Tom Longson - My first guinea-pig, some three years ago...
  • Malaney J. Hill - We demystified the m.e.s.s. in code: modes, exceptions, states, and steps.
  • WF2 crew - Your horrifically frustrating legacy code started all of this.

License

Salt is available under the terms of the MIT-License.

Copyright 2014, Bemi Faison

Bitdeli Badge

salt's People

Contributors

bemson avatar bitdeli-chef avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

salt's Issues

Consolidate logic, reduce file size

The recent work done for the 0.5 milestone produced a lot of code. The two features which needed the largest number of helper functions, were: sub-instances (issue #25) and .prepNode importing (i.e., merging branches before compilation).

Flow was minified and gzipped and still measures more than 5K. (Panzer itself is about 6K - but that's another issue for that repo.) In the end, with all of it's features, Flow should be under 3K. These features should be reviewed, refactored, and optimized, when time permits.

"_restrict" component allows targeting the parent of restricted states

The _restrict component lets a state restrict external program access to descendant states. However, Flow permits public access to both the parent and descendant states.

The following code demonstrates using the _restrict component.

var ajaxBtn = new Flow({
  _restrict: true,
  _main: function () {
    doAjaxThen(ajaxBtn.handleResponse);
  },
  handleResponse: function (rsp) {
    // do something with ajax response
  }
});

Above, after entering the program's root (e.g.,. by calling ajaxBtn), only the "handleResponse" state should accessible. However, when we target the program root consecutively, Flow repeatedly executes the _main component-function - effectively ignoring the _restrict component.

Upgrade to genData 3.x

Though Panzer depends on genData internally, Flow depends on it directly for program compilation. Because both Flow and Panzer depend on genData, when Panzer updates it's version of genData so should Flow, in order to avoid conflicting versions of genData in the same environment - specifically the web browser.

Add managed `.state` property

As a state-machine, Flow only provides access to it's current state via the .status() method. The returned "status object" contains information about both the current state and the navigation sequence. The state-related values are static (e.g., only change when the state transitions), and the navigation values are calculated - or transposed. When idle, the navigation values are usually empty, which means half the calculations are likely unnecessary.

This issue proposes to define a new member for describing the static information about the current state. The .state member will be a single object, mutated over the life of a Flow instance. It will contain the following properties:

  • name
  • path
  • index
  • phase

These properties will be updated as the Flow traverses it's program. The goal is to manage this property, such that it can be referenced directly; the object-member would not be dereferenced by Flow.

Third-party integration

Other packages would be able to add-to or override properties of the .state member. By convention, package authors will be encouraged to not replace the .state member - but instead, add and remove it's properties.

Improve api for sub-instance management

As a managed runtime environment, Flow supports sub-instance capturing. A sub-instance is a flow that gets instantiated by another flow's traversal-event callback.

This issue seeks to define a better api for capturing, accessing, and removing sub-instances.

Current Implementations

Manual

Without additional features and functionality, sub-instances are managed like any other variable collection. Below demonstrates collecting sub-instances without any specific Flow api.

var parent = new Flow({
  _data: {
    kids: []
  },
  add: function () {
    var kid = new Flow();
    this.data('kids').push(kid);
  },
  count: function () {
    console.log('# of sub-instances:', this.data('kids').length);
  }
});

parent.go('//add', '//count');
// # of instances: 1

Flow v0.4.x

Flow 0.4.x took advantage of Flow's managed execution, and enabled the automated capture of new Flow instances.

The tag _store let you specify which instances should be captured. The method .store() let you access captured instances or add them manually. Below demonstrates how both additions reduced the overhead from the first example.

var parent = new Flow({
  _store: true,
  add: function () {
    new Flow();
  },
  count: function () {
    console.log('# of sub-instances:', this.store().length);
  }
});

parent.go('//add', '//count');
// # of instances: 1

This is exactly how Flow is meant to isolate and reduce the overhead of implementing common procedural patterns. By enabling instances to store sub-instances, developers no longer need to define and manage the contents of a custom collection.

The full API of the 0.4.x approach has never been documented. However it has proven confusing in terms of both taxonomy and implementation. I'd like to define an improved API and taxonomy in Flow version 0.5.0.

Privileged member for private'ish sequence variables

In order to make it seamless and easy to pass values between states, without worrying about exposure to external routines, Flow should introduce a privileged instance member. This issue is about defining implementation rules for this feature.

Add .path property to core-package nodes

With the change to Panzer 0.4.x ( issue #14 ), Flow must now declare the path of each node. This must be done during the initialization of the core-package's instance, via the <pkg>.init() method.

Implications

Similar to Flow's built-in package, named core, additional packages can no longer expect node's to contain a local .path string. In the future, core package, may need to updated with friendlier access to it's nodal properties.

nodes names toString are inaccessible

Because Flow uses the toString method to identify map functions, it must be protected. Until then, nodes name toString are processed (compiled), but inaccessible within the map-function.

This will be addressed in upcoming versions.

Revise `callbacks()` to not return a function-list

The "function-list" feature harks back to Flow's beginnings, where dot-syntax access seemed sexy. While useful, the pre-compilation required to support this feature, and the discrepancy between return types (see #28), make it a less attractive method.

The new .callbacks() will no longer return function's that are part of a function-list. Instead, the method will accept these arguments:

  1. query - a program query, indicating where the instance should navigate
  2. waypoint - a booly flag, specifying when the curried function will invoke .target() or .go(). Default is false
  3. blessed - a booly flag, indicates when the curried function ignores all permissions. Default is false

With no arguments, the returned function will target the program state. In all cases, the returned function may still be used as a query parameter, but will no longer reflect a function-list.

Rationale

The function-list, currently compiled per Flow instance, duplicates the entire program structure with corresponding curried calls to .target(). By removing this function list - meant only for the .callbacks() method - compilation time should improve with the reduced number of objects created.

Code a demo package

Flow is very extensible, and offers a unique delegation architecture for adding powerful features and enhancements. In order to encourage third-party development, the authoring community needs working demo code.

This issue is for listing non-trivial extensions to Flow, then choosing one(s) that demonstrate the following:

  • define a package-instance
  • add a proxy method
  • direct/intercept navigation events
  • integrate with other packages
  • write code that passes current unit tests

The selected packages will ultimately have their own repositories (for easy cloning, and for use as a template package design).

States identified with integers lose their ordering


The format for defining a Flow's program relies on an expectation that the JavaScript engine will preserve key order. However, this is not the case for integer keys, which results in an inaccurate compilation of the intended program.

The code below demonstrates the issue.

var tree = new Flow({
  '2': {},
  '1': {}
}, true);

console.log('Last child is ', tree.query('@child/@oldest'));

The output expected is Last child is //1/

In FireFox v3.6, the output is Last child is //1/

In Chrome v10, the output is Last child is //2/


The underlying issue is that there is no ECMA guideline mandating the preservation of object key order. On one hand, this advises some developers to avoid relying on key-order. Others, like myself, feel the consistent implementation of preserving key-order, by JavaScript all engines, constitutes a standard in and of itself. The single exception, is when the key can be coerced to an integer.

The underlying issue is currently under debate - though not necessarily under review.


Below are some options for avoiding this issue:

  1. Ignore integer keys at compile-time
  2. Use an alternate format for the program parameter
  3. Introduce API methods to build and compile a program

Ignore integer keys at compile-time.

This is the quickest solution. There are already rules for naming states (e.g., no initial underscore), and the discrepancy between JavaScript engines is avoided

Use an alternate format for the program parameter.

A hybrid Array/Object-collection format can be used to define parent-child-sibling relationships between keys. The format might look something like this:

var flow = new Flow({
  inFnc: aFnc,
  outFnc: aFnc,
  mainFnc: aFnc,
  children: [
    {
      name: 'firstState',
      inFnc: someFunc,
      outFnc: someFunc,
      children: [
        ...
      ]
    },
    {
      name: 'secondState'
      ...
    }
  ]
}, true);

This is similar to other mature nesting structures, such as the configuratin object for YUI's TreeView.

Introduce API methods to build and compile a program

This was a predicted feature for Flow, which means development would support vertical (instead of horizontal) progress. Flow would introduce an API for building programs programatically; state by state, component by component. The program could then be compiled, or passed to a Flow for compilation. Again, YUI's TreeView widget creation process is a model candidate for such an API, along with standard DOM creation methods.


Please share your comments for addressing this issue.

RJS package: integrate require.js

The "rjs" package would implement convenience tags and methods, for loading AMD modules with require.js. This issue list possible use-cases and implementations, of the RJS package.

The package may exist in a separate github repository.

Add tag to deny untrusted routines from accessing program states

Currently, two tags determine how untrusted routines may access a program:

  • _ingress, which limits when a brach may be accessed
  • _restrict, which denies leaving a branch

What seems to be missing (and necessary) is a tag that makes a program branch inaccessible by untrusted calls.

This would prevent untrusted routines from navigating to sensitive states, like myFlowGame.target('//user/score/add/', 1000000);. With the proper restrictions, such a call could only be invoked by the program itself - or a (sanctioned) nested Flow.

In this sense, "hiding" a state is a way to make them as private (but from whom). The affect would cascade to descendants, and each state should be allowed to unhide itself and it's branch.

Invocation "trust"

When a Flow instance method is invoked, there are three levels of "trust" (the labels will likely change):

  1. trusted: The instance is actively navigating program states.
  2. internal: The instance is active, but awaiting a nested-instance to complete navigating.
  3. external: The instance is inactive or has paused navigation.

External calls are considered "untrusted". Depending on the level of access/control a method exposes, internal calls might also be considered "untrusted". Flow lets methods incorporate these trust levels in their logic, which equates to access and control of an instance.

In light of trust levels, which sould allow accessing a hidden state (besides internal)? Should external calls never have access, and internal calls be given access to them?

Splitting hairs here means either:

  • two tags: one to hide states externally and another for hiding states internally
  • one tag that allows defining "visibility" for both trust-levels

Deprecate "cedeHosts" and "hostKey" configuration options

Flow 0.3.x features a security scheme, based on comparing host and key values. At instantiation (after the program parameter) you can pass in the configuration options cedeHosts and hostKey.

The cedeHosts key would accept an array of acceptable "keys". If another instance were defined with a matching hostKey configuration option, it could control the first instance regardless of it's locked status.

The use case: In a mashup environment, a locked Flow may need to be controlled or accessed by another instance, but still maintain it's independent execution. In order to filter unwanted executions, the calling instance would need have a "key" for privileged access.

Rationale

Since using and enhancing Flow, I've realized that this approach is not only poorly architected, but very short sighted and rather limiting for program authors. For example, the scheme only works between flows, which limits it's usefulness in a true mashup environment; where any JavaScript statement might direct a Flow instance.

As well, there are already several tags for restricting navigation. Of note, as tags, they become part of the program's structure, not an overriding aspect which could disrupt the original implementation.

The concept of ceding control of your code from to a "backdoor", goes against the well architected, self-regulating programs that Flow is meant to encourage.

Fails unit tests in node v0.11

While integrating with travis-ci.com, it seems Flow can not pass unit tests on node v0.11. This issue should track progress in debugging the core problem. In the interim, the travis-ci config was updated to ignore this environment.

One caveat: It may be that v0.11 is an edge version of NodeJS, since nodejs.org lists the current version as 10.5. (The Mocha based tests were augmented to pass there, but those issues involved how the sinon components were being used.) Thus, this issue has a lower priority.

Remove `.bless()`

The .bless() method was created so external routines could control a Flow instance despite it's lock status. Below are one of the initial use-cases, which brought about it's creation.

var flow = new Flow({
  _in: function () {
    $.get('some/ajax/path', this.bless(function (data) {
      if (data == 'ok') {
        this.go();
      } else {
        this.go(0);
      }
    }));
    this.wait();
  },
  _on: function () {
    console.log('ajax completed with "ok!"');
  }
});
flow.go(1);

The problem is that .bless() encourages using anonymous functions, or - worse still - external functions to control Flow. This goes against the idiom of using state's to describe the sub-states of a branch and their relationship. In other words, the .bless() method, let you treat any function as part of the Flow program, while not actually being part of the program.

This issue will track it's removal from Flow.

Providing greater access/insight into executions

As an execution framework, Flow controls access to functions... and their output. During a navigation sequence, traversed states invoke their callback functions. If a sequence is begun/resumed via the .target() method, it returns the result of the final (destination) state's _on callback.

The rules controlling when a navigation sequence returns a callback function's result, deny potentially valuable returns from other callbacks (e.g., _in, _out, waypoints, etc).

This issue is about finding a way to expose the return values from callbacks, during a navigation sequence.

core: _via

The _via tag is meant to the restrict access and execution of a state. When present and assigned one or more flow queries, this state can only be targeted by matching states. As well, during navigation this state will only execute when the navigation route includes a matching state.

Below demonstrates how a _via tag can further narrow the scenario for executing a state.

var myApp({
  ajax: {
    _on: function () {
      $.ajax({
        url: '/path/to/async',
        failure: this.cb('failed');
      });
      this.wait();
    },
    failed: {
      _via: '@parent',
      _on: function () {
        console.log('The ajax failed!');
      }
    }
  }
});

.lock(setting) sets opposite of flag

The lock() method is supposed to interpret a value to set the Flow's locked state. Unfortunately, tests didn't reveal how a truthy argument unlocks the Flow, while a falsy argument locks the Flow... The opposite impact intended with this method.

The fix is simple, but tests must bear out the patched file.

Improve description and usage in README

The current README doesn't properly introduce Flow or give a decent overview of it's usage.

The README should remain short while including (at minimum) a description and usage section. The description should be no more than two paragraphs, and explain:

  1. What is Flow?
  2. Why is it novel?
  3. How does it work?

The usage section will contain a trivial walk-through of a code sample. The goal of the usage section is to demonstrate how Flow simplifies common procedural patterns.

This issue will be updated to capture possible starts to the primary README sections.

Refer to the initial state as "null" instead of "flow"

In order to reduce confusion and clarify terminology, the built-in token for targeting the first state of a program should be @null instead of @flow.

This term is more appropriate, since the first state can not be modified and represents the state of a Flow instance, before any logic has occurred.

Same for the null state's name "_flow"

The null state's name should also be change from "_flow" to "_null".

Import fails when source is a (short-form) string targeting a (short-form) function

The the first short-form may not matter, the latter is not being converted to an object. Instead, the function is being used. The problem is that genData only iterates over objects, and so the referenced function is ignored.

A new test was added, which ensures this use-case is satisfied.

describe( '_import tag', function () {

  it( 'should assume an imported (short-form) function is the _on callback', function () {
    var spy = sinon.spy();
    flow = new Flow({
      a: '//b/',
      b: spy
    });
    flow.go('//a');
    spy.should.have.been.calledOnce;
  });

});

Remove submodules in favor npmjs.org registry

Flow depends on both genData and Panzer libraries, which have been included as submodules of this repository. When fetched recursively, the file space used for developing Flow becomes cluttered, resulting in duplicate files and unintended or uncommitted changes to dependencies.

To encourage focused development of Flow code and reduce the general cruft from submodule code, when possible, dependencies will be loaded as npm modules.

As an npm module, developing on the Flow repository requires a second step: installing the dependent modules. This is simple enough - if after first installing npm - you run npm install from your system's terminal, from the root directory of the Flow repository.

Rationale

The files loaded by npm are placed in a "node_modules" directory. Flow's .gitignore file is configured to ignore this folder and it's contents. The result is the complete separation of dependent and core development resources. Also, the size of the repository will be reduced, for simple development changes; a full development environment still necessitates downloading dependencies, of course.

In the end, this issue centralizes the burden of (submodule) version management, to a package system built for just that.

Considerations

  • Dependencies can be managed from one place, the package.json file.
  • Using and testing Flow now requires installing the npm modules; again, an additional step after cloning the repository.
  • Test files which previously loaded dependent resources from the "src" folder, will need to target the "node_modules".
  • Submodules have their place. Specifically, when the dependency is not an npm module. Or, when the development environment requires third-party code.

Can not define states with spaces

The following code program fails to resolve into the expected paths.

var flow = new Flow({
  'hello world': {}
});

flow.query('//hello world'); // false (should be truthy)

While states cannot contain the characters ".", "/", "|", or "@", spaces should be allowed. The offending line looks to be the core package's .badKey member; a regular expression that flags anything that is not an alphanumeric character. The fix should be simple enough, without resorting to making .badKey a function (which could significantly slow compile times).

proxy.callbacks() are inconsistent when query is absolute vs resolved

The .callbacks() method of Flow instances returns a different function based on the string format. When the string matches the full path of an existing state, it returns that state's callback function. All other strings are assumed to be dynamic paths (to be resolved later), and returns an anonymous function.

A state's callback function is augmented to reference it's descendant state's callback functions - the rest of it's program tree. The anonymous function has no such structure appended to it.

Given the following flow...

var app = new Flow({
  here: {
    and: {
      there: {}
    }
  }
});

...and the following callbacks...

var goHere = app.callbacks('//here/');
var findHere = app.callbacks('//here');

...it's easy to miss the difference between the function's goHere() and findHere(). The first has the remainder of it's states attached, like so: goHere.and.there(). However, the second is a simple anonymous function, and attempting the same would cause an error.

The difference is subtle and dangerous. A clear spec would help here, so users would understand and expect one or the other type of function, based on sending a string - not the string's value.

Event tags paired to strings or numbers should compile as redirect queries

In cases where an event tag (like _on, _out, etc.) is used to redirect a Flow instance, authors must use individual anonymous functions containing one .target() or .go() method. To isolate and eliminate this pattern of procedural usage, Flow should support automatic redirects when an event tag is paired with a string or number. (The string would be interpreted as a query and a number would be interpreted as a state index.)

The state target would not be validated until the event callback executes as normal. Internally, automatic redirects will use the .go() method for routing. The implementation would use an internal shared function, which would go towards reducing the memory footprint of a Flow instance.

getFlow does not accept flow ids, only map-functions

The static method Flow.getFlow should be able to accept a string for retrieving Flow instances with the same id. The regular expression to test a given string, does not match the rules for a Flow id, and must be updated.

Make tail navigation declarative

This functionality captures a procedural pattern, where more states direct Flow to the same ancestor. This pattern seems to show up when handling user events, and/or when a state serves as a switch before navigating to descendant states.

For instance, if a user action leads to choosing a random number, we might do this:

var number = new Flow({
  _on: function () {
    console.log('Pick a number!');
  },
  pick: {
    _on: function () {
      var childStates = ['one', 'two', 'three'];
      this.go(childStates[Math.floor(Math.random() * 3)]);
    },
    one: function () {
      console.log('You picked', this.status().name + '!');
      this.go('@root');
    },
    two: '//pick/one/',
    three: '//pick/one/'
  }
});

As you can see, the states under //pick/ would return to the root state. It's these kinds of procedural patterns that Flow is meant to address via (declarative) tags.

Core _tail

The core package may introduce a _tail tag, which would provide the following functionality:

  • Direct Flow to the same or non-descendent state, when navigation ends in a branch.

The concept is pretty simple, and allows rewriting the orginal example as follows:

var number = new Flow({
  _on: function () {
    console.log('Pick a number!');
  },
  pick: {
    _tail: '@root',
    _on: function () {
      var childStates = ['one', 'two', 'three'];
      this.go(childStates[Math.floor(Math.random() * 3)]);
    },
    one: function () {
      console.log('You picked', this.status().name + '!');
    },
    two: '//pick/one/',
    three: '//pick/one/'
  }
});

This trivial example ends up trading the procedural .go() line with the declarative _tail tag. However, the value of the tag increases along with the number of descendent states.

Implementation Rules

The _tail tag's functionality cascades along it's branch. However, descendent states can use their own settings to impact themselves and their branch. The tag takes the following values:

  • true - Desigates a state as the destination, for when navigation ends within it's branch.
  • false - Excludes a state (and it's branch) from any ancestor's setting.
  • <Flow path> - The resolved state will be targeted when Flow stops within the owning state (or it's branch). If the state is invalid or unresolvable, the tag is set to false.

When set to a path, the state must not be any of the following:

  • A descendent state
  • An ancestor state that is also sequence

Deprecate .data() method in favor of a .data property

This issue proposes to keep the _data tag and replace the .data() getter/setter method with a simple .data object. The tag would preserve the default value and scoping features.

This proposed change represents the first property applied to the proxy object - the instance returned from new Flow(). Due the internal/closured structure of a Flow instance, the implementation must update the .data attribute for all package-instance proxy objects. The underlying changes to the Panzer dependency, will make this easy to implement.

Background

To date, the .data() method has been an amalgamation of keys defined by the current state and it's ancestors. Flow has allowed a kind of scoping for data keys in nested states, such that the parent state's value is less likely to be overridden by actions of descendent states.

The following demonstrates the current behavior of both the tag and method.

var task = new Flow({
  _data: 'foo',
  _in: function () {
    this.data('foo', 'bar');
  },
  nested: {
    _data: 'foo',
    _on: function () {
      this.data('foo', 'zee');
    }
  }
});

task.go('//nested/');
console.log(task.data('foo'));  // zee
task.go('@parent');
console.log(task.data('foo'));  // bar

As you can see, the data feature manages what the value of a data key is based on the current state.

Changes & Rationale

While, the Flow architecture allows methods to base their behavior on the current state and whether the Flow is active (navigating), the data feature was designed to behave the same in every scenario. This means the getter and setter nature of .data() method has never been employed to prevent retrieving and assigning keys and values.

The proposed change would allow accessing data properties directly from the Flow instance. Below demonstrates the future behavior of a Flow instance, using a .data property.

var task = new Flow({
  _data: 'foo',
  _in: function () {
    this.data.foo = 'bar';
  },
  nested: {
    _data: 'foo',
    _on: function () {
      this.data.foo = 'zee';
    }
  }
});

task.go('//nested/');
console.log(task.data.foo);  // zee
task.go('@parent');
console.log(task.data.foo);  // bar

As you can see, retrieving and assigning values to the data object is less tasking than with a getter/setter method. As well, the values can be updated (internally), exactly as before.

Implementation Considerations

The proposed .data property would need to be local for every package-instance's proxy object. (A "Flow instance" is the proxy object of the last package for Flow - the default package is named "core".) As a local property, the .data property is at risk of data tampering.

Efforts to avoid data tampering are out of scope for this change, but it may become a concern later on. The best way to handle tampering is to reassign (or re-copy) an internal object, as the proxy's .data property, whenever a Flow event occurs - or, perhaps during traversal events only.

Package to define and execute routes

Flow needs an api to support route like paths. This functionality should be added via a new unnamed package. Possible package names (so far) are "router" and "path".

Routes are similar to the Flow path syntax, except they often reflect a user-friendly, browser url (rather than an internal code-path) and can be interspersed with parameters.

This issue will look at how to implement the following example route:

/blogs/:blog_id/highlight/:term/:color

Support AMD environments

Flow should support modern JavaScript runtime environments, such as codebases using AMD loaders, like require.js.

This must also take into consideration, CommonJS environments which share the require() method, but implement it differently (one is asynchronous while the other is synchronous).

Flow should therefore support the following environments:

  • CommonJS (i.e., Node.js)
  • AMD (i.e., require.js) - includes running within a CommonJS environment.
  • Browsers (including those using require.js)

Boilerplate

Below is the "boilerplate", or template structure that satisfies these runtime conditions. (Assume the module being loaded is called Lib).

!function (inAMD, inCJS, scope /*, errata */, undefined) {

  function initLib(require, exports, module) {
    var Lib = {/* library code - could use "require()" synchronously here */};
    return Lib;
  }

  if (inAMD) {
    define(initLib);
  } else if (inCJS) {
    module.exports = initLib(require);
  } else if (!scope.Lib) {
    scope.Lib = initLib();
  }
}(
  typeof define == 'function',
  typeof exports != 'undefined'
  /* errata: closured globals and initialization checks, needed by initLib() */,
  this
);

This approach prefers AMD over CJS, since the former can be used in the latter. It's the best boilerplate I could muster, without having to define a build system targeting each environment.

Flow and nodejs

There are several issues when using flow within nodejs.
For one, it would be nice if flow could be integrated as a module (npm would be great! :))
Therefore it would also be necessary that it wouldn't expect GSet in the it's scope but rather require it too.

The other thing is, it expect's this to be a window. This is not the case in nodejs.

I worked around all of this by adding the following lines:

GSet = require('../deps/gset-min.js').GSet;
this.clearTimeout = global.clearTimeout;
this.setTimeout = global.setTimeout;

I've got one more issue where _main isn't called, i'll try to debug into this.

Just so you know :)

Ullrich

Upgrade to Panzer 0.4.x

The Panzer dependency is undergoing a major overhaul, promising better performance, increased interoperability between Panzer class packages, and introduces several new events.

Flow will take advantage of this with a major code refactor. While Flow's public API is impacted slightly, the real change comes to authoring packages that define and enhance Flow.

Remove state-related properties from `.status()`

With the introduction of the .state property (Issue #38), the .status() method should no longer return static data-points - state information specifically.

The following items should be removed from the returned status object:

  • pendable
  • path
  • state ("name" in the .state property)
  • index
  • depth
  • phase

Recursion protection limits use of Flow

The Fibonacci use-case scenario demonstrates how Flow could be used, but the internal recursion check blocks this type of implementation. Removing the recursion is best - allowing standard recursion controls to derive from the JavaScript engine itself.

go() clears previously set waypoints

The go() method clears previously set waypoints, when called internally or externally.

The following component function, demonstrates the issue when handled internally. The _in component does not route the navigation to "b", as expected.

var page = new Flow({
  _in: function () {
    this.go('b');
    this.go();
  },
  _main: function () {
    console.log('fired A second!');
  },
  b: function () {
    console.log('fired B first!');
  }
});
page(); // -> fired A second!

The following function, demonstrates the issue when called externally. The statement does not route the navigation to "b", as expected.

var page = new Flow({
  _in: function () {
    this.go('b');
    this.wait();
  },
  _main: function () {
    console.log('fired A second!');
  },
  b: function () {
    console.log('fired B first!');
  }
});
page();

Flow(page).go(); // -> fired A second!

The cause is that the staged waypoints are not committed after exiting the component function, and the go() method blindly sets the given waypoints, even if the waypoints array is empty; this is not the expected behavior.

This issue should be addressed in the next release.

Migrate to the Mocha test framework

In order to better support continuous integration (or "CI") environments, and improve testing versatility, Flow should use a test framework that supports the following:

  • command-line support
  • browser
  • automated testing methodologies, like (e.g., CI, BDD, TDD, etc.)

Currently, the test suite of choice is Mocha, which supports both BDD and TDD methodologies, and is compatible with many assertion-based libraries.

An improved test framework is also necessary to work with travis-ci.org, which support command line testing of JavaScript code. (This is related to issue #18)

Background

As of filing this issue, the current test framework is qUnit. That framework is showing it's age, is rather large, and requires too much overhead to work with command line interfaces; it is primarily a browser-based test framework.

Allow passing arguments to the `.wait()` callback

The .wait() method fires a given callback when the delay expires; the callback can also be another program state. In both cases, the only way to pass arguments to the callback, is to store it in a shared (i.e., public) location.

The example below demonstrates how the .data property must be used as a bridge between the current navigation and the .wait() callback.

var delayCall = new Flow({
  _data: 'fnc',
  _on: function (callback) {
    this.data.fnc = callback;
    this.wait('invoke', 0);
  },
  invoke: function () {
    this.data.fnc();
  }
});

Support pass-through arguments in .wait()

The .wait() method could support passing-through arguments to the callback. The method would pass all arguments after the second, to the given function or navigation target.

Below, demonstrates the earlier example, using additional arguments.

var delayCall = new Flow({
  _on: function (callback) {
    this.wait('invoke', 0, callback);
  },
  invoke: function (callback) {
    callback();
  }
});

This approach removes the "glue" code needed, when passing values asynchronously.

Deprecate the match-set token's bracket wrapper ("[]")

The match-set token signaled when part of a path should consider more than one value - when resolving a Flow path/query. An example of match-set token usage follows:

someFlow.query('//here/[foo|zee|there]/everywhere/');

The hard brackets ([]) were used to indicate that the path/query part had multiple options.

Since the path syntax prohibits a token or state name from including the pipe-character, I think the presence of a pipe character is enough to indicate that there are multiple options. The wrapper feels unnecessary.

Without brackets, the initial query would now look like so:

someFlow.query('//here/foo|zee|there/everywhere/');

While there is less structure to this form, it should result in a reduced amount of parsing overhead, and symbol noise.

Change method sub-instance method and tag names

Commensurate with changes to sub-instance management (see #25), the method and tag used should reflect the update terminology.

.flows() renamed to .subs()

The .flows() method deals with a Flow's collection of sub-instances. The term "sub-instances" feels right, so the method name would better represent the object's it operates on as .subs().

_store renamed to _captures

The _store tag was introduced when .flows() was named .store(). Since the tag had always been about capturing sub-instances, the named should be _captures.

Formalize and refactor permission's scheme

Flow provides an introspective execution framework, where methods can gate their functionality based on various criteria.

  • Stack The framework manages an execution stack between navigating Flows, tracing the order in which instances await completing navigation.
  • Ownership Instances may be owned or own one another. This relationship can be used for security checks/overrides.
  • Locked Instances may lock themselves. While originally designed to control external access, the locked status of an instance has evolved to have internal application.
  • Active Instances may be idle or active (i.e., currently executing). This ensures methods like .wait() will only pause navigation when the Flow instance is navigating.
  • Current Active instances, first in the stack, are considered trusted, since they are assummed to be in control of (if not, the reason for) all executions.
  • Opposing criteria, such as "not locked" and "not active", etc.

I'd like a simple way for methods to ask, "Am I an active and locked instance?" This issue seeks to enable simple confirmation of all security criteria.

Integrate Travis-CI

By adding a simple yml configuration file, Flow may benefit from proven, public, and persistent testing. The hook process was painless for this repositories dependencies: Panzer and genData.

Add "subs" call group to permissions scheme

Sub-instances may be granted special permission (see #35) when invoking other flows. As such, their call group should be recognized within Flow. Currently, they are lumped in the "world" group.

Below is a use-case where granting distinguishing sub-instances form the world might be desirable.

var form = new Flow({
  validation: {
    _perms: ['!world', 'subs'],
    _ingress: true,
    _on: function () {
      this.subs().forEach(function (formControlFlow) {
        if (!formControlFlow.target('//validate') {
          form.go('invalid');
        }
      });
    },
    invalid: function () {
      console.log('missed something in the form!');
    }
  }
});

The above example shows a form instance that denies all but sub-instances to redirect it, on failure. This allows each sub-instance to have their own validation logic, and ensures the master form does not go to the invalid state directly (with help from the _ingress tag).

Again, because the "world' group is denied access, we need a "subs" group to distinguish the calling Flow. The default for this group would be true - as it is with the "world" group.

Only capture sub-instances created by another Flow

Sub-instance capturing currently relies on whether there is an active Flow. But this doesn't consider where the Flow was created, and could lead to inadvertent capturing.

Consider a utility function that uses Flow internally. Calling that utility within a Flow callback, would cause that utility's Flow to be captured.

The fix proposed, would check the call stack, to ensure the newly created Flow is only captured when it's created by another Flow's callback (i.e., _in, _out, etc.)

wait() does not pause Flow after calling target()

After inserting wait() into the Fibonacci use-case, the numbers should be consoled-out slower. However, calling .wait() after .target() has no effect. The code below demonstrates the bug.

 fibonacci = new Flow({
  _main: function (n, prev, cur) {
   console.log(cur);
    if (n--) {
      this.target('@self', n, cur, cur + prev);
      this.wait();
    }
  }
});

fibonacci(6, 0, 1); // -> 1

Only one number should be in the console, when this code is run.

Use the _import tag with objects

The _import tag allows you to reuse states within the same program. This limits what can be shared to what's already part of the program. Instead, and more intuitively, the _import tag would allow you to reuse external object references.

Use Case: Template programs

Two or more flows share a common program, but need to augment a specific state and/or attribute. Below demonstrates how a state's structure may be shared.

var baseAlertProgram = {
    _data: {msg: 'hello world!',
    _on: function () {
      alert(this.data.msg);
    }
  },
  alert1 = new Flow({
    _import: baseAlertProgram,
    _data: {msg: 'foo!'}
  }),
  alert2 = new Flow({
    _import: baseAlertProgram,
    _data: {msg: 'bar!'}
  });

alert1.go(1); // alerts "foo!"
alert2.go(1); // alerts "bar!"

baseAlertProgram represents a template (similar to an abstract/base-class) for displaying a user "alert". The import process allows a flow to overwrite any tag or state of the imported object. In this case, because the _on tag uses the "msg" data point, it's easy to change what the msg says.

Value: Very high

Shared program definitions, that are easily augmented, makes it easier to choose Flow for large, complex, repetitive systems.

Introduce tag that allows querying states directly

As with the id attribute in HTML elements, Flow should allow developers to identify states of their program. The identifier would then be available as a custom token in a Flow query string, as a means of directing an instance towards a given state. Custom identifiers would also reduce the length of query strings.

This would be best implemented declaratively, as a tag. The proposed tag name is _name (and _id is a close second).

The example below demonstrates how the _name might be used in a program.

var app = new Flow({
  deep: {
    deep: {
      'in': {
        it: {
          _name: 'here',
          _on: function () {
            console.log('you are here!');
          }
        }
      }
    }
  }
});

app.go('@here'); // you are here!
// alternatively, you could invoke

// app.go('//deep/deep/in/it');

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.