Git Product home page Git Product logo

pipeline's Introduction

Pipeline

A tiny Flux framework with strong opinions.

About

Pipeline is a framework for creating applications based on the Flux architectural pattern.

Pipeline isn't designed to to solve everyone's problems; its purpose is to see how far the conceptual straightforwardness and declarative nature of Flux can be taken. This means it is straightforward when writing an app, but also straightforward when you, your team, or new people find your code much later and need to figure out what it is doing. Particular parts of Pipeline may feel verbose at first, but the verbosity helps later down the road.

Parts of a Pipeline app

Pipeline apps are composed of three conceptual components: actions, stores, and adapters.

Actions

Actions are the means by which data enters your system.

Actions take arguments and optionally validate them. Their arguments are passed on to the callbacks of stores which listen to them.

An app can only respond to one action at a time. Trying to send a new action while another is still being dispatched will result in the new action being deferred until the current dispatch is ended.

Important note: Everything within an action's packager should be synchronous.

Stores

Stores maintain your application state in one place.

Information has exactly one way into a store after the app has started: actions. The only thing that can alter the state of a store is that store itself. After the app has started, the only time the contents of a store can change is during its reaction to an action.

The only public interface with a store is through a get() method and any custom getters defined under the api option.

Stores can get from other stores within an action callback. This is allowed by specifying that the store must wait for those stores to react to the action. Store evaluation order per-action is sorted ahead of time. If a store does not listen for an action, it should be available to any stores that respond to that action.

Important note: All store state mutation must occur synchronously during the response to an action.

Adapters

Adapters are how a Pipeline app talks to the outside world.

Adapters react to changes in stores they listen to. They may also react to changes that happen external to Pipeline, such as user actions or external API calls.

A view which renders data in response to a store is one type of adapter. Pipeline comes with a mixin for React.

How these things are used

Actions

An action is created by specifying a name and, optionally, a validator. The validator is a function which is used to validate the data the action carries.

Stores

A store is created by specifying a name and a set of options. The options include the set of actions a store listens for and the callbacks to run when the corresponding actions are dispatched.

Adapters

Adapters are created by specifying a name and a set of options. The options include the set of stores an adapter listens to and the callbacks to run when the corresponding stores update themselves.

Installation

bower install pipeline or npm install pipeline-flux

Plugins

Pipeline can be extended through the use of plugins. The plugins API is still being fleshed out, but two example plugins have been created already:

  • pipeline-routing - Routing and navigation with an action, a store, and an adapter.
  • pipeline-react-views - Offers a shorthand for making React components which function as adapters.

To use a plugin, pass them in on the options argument when you instantiate the app, like so:

var pipeline = require('pipeline-flux');
var routing = require('pipeline-routing');
var views = require('pipeline-react-views');
var react = require('react');

var app = pipeline.createApp({
  plugins: [
    routing,
    views(react)
  ]
});

pipeline's People

Contributors

lubelski avatar rimunroe avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

pipeline's Issues

create implicit actions from stores

If an store listens to an action 'foo', but app.create.action('foo') is not explicitly called, then the app creates an implicit action.

app.create.action('foo') creates an explicit action.

if two store try to listen to an action that isn't registered explicitely, then the app will warn/throw on app.start()

accessing flux objects via certain contexts

Pondering what should be easily available when.

From within an store's action callback:

@action is the action payload as packaged by the action packager.
@stores is a ref to the App.stores, so that @stores.fooStore.getFoosByID(id) can be called without needing to know about a global reference to the App itself.

From inside of an adaptor's callback for a a store change event:

@stores is available to get @stores.fooStore.getFoosByID()

@actions is funny, because calling them directly in the CB would result in nothing happening because the dispatcher wouldn't dispatch them.

If for some reason we wanted to send 'immeditaly-ish', we could have a function like @enqueue(@actions.fooAction) and it would wrap a setTimeout or something like it.

From inside of an mixin'd react view's callback for a store change event:

Going along with the idea that views shouldn't necessarily be part of App, views might want to reference the app via an actual variable.

Question: Do we care about having multiple apps? i.e. one view can subscribe to change events on one app and change events on a totally unrelated app?

Option #1: Mixins Per store. Global Refs all the way

FooView = React.CreateFactory React.CreateClass
  displayName: 'Foo View'
  mixins: [App.stores.fooStore.mixin, App.stores.boopStore.mixin]
  getInitialState: -> 
    foo: App.stores.fooStore.getFooByID(id)
  onFooStoreChange: -> 
    @setState foo: App.stores.fooStore.getFooByID(id)

Option #2: Mixins per store, set up store refs in the view

FooView = React.CreateFactory React.CreateClass
  displayName: 'Foo View 2'
  mixins: [App.stores.fooStore.mixin, App.stores.boopStore.mixin]
  getInitialState: -> 
    foo: @fooStore.getFooByID(id)
    boops: @boopStore.getBoops()
  onFooStoreChange: -> @setState foo: @fooStore.getFooByID(id)
  onBoopStoreChange: -> @setState boops: @boopStore.getBoops()

Option #3: 1 Mixin per app + store keys, set up single stores ref in the view

FooView = React.CreateFactory React.CreateClass
  displayName: 'Foo View 2'
  mixins: [App.ReactMixin(boop: 'onBoopChange', foo: 'onFooChange')]
  getInitialState: -> 
    foo: @stores.foo.getFooByID(id)
    boops: @stores.boop.getBoops()
  onFooChange: -> @setState foo: @stores.foo.getFooByID(id)
  onBoopChange: -> @setState boops: @stores.boop.getBoops()

action validation syntax 2.0

The action validation syntax is awesome.

current:

app.create.action 'actionName', (arg1, arg2) -> 
  @require(_.isString(arg1), "arg1 must be a string)

Potential Improvement:

app.create.action 'actionName', (arg1, arg2) -> 
  @require _.isString(arg1), "arg1 must be a string"
  @suggest _.isBoolean(arg2), "arg2 should be a boolean"

name tbd. The idea where is that you want to let some thing slide but raise concern. if arg2 should be a boolean, but some truthy or falsey value is being passed then the app designer might want the system to still work, and then they can go and fiddle with the call site and coerce it to boolean or whatever they want to do.

This is sort of patterned off of react's array key warnings.

better this.isMounted() check on change events

When higher level component changes due to a store subscription, it may unmount a lower level component, even if that component has subscriptions.

That lower level component's subscription callbacks may be run before componentWillUnmount is called to remove those subscription callbacks.

Checking this.isMounted() appears sufficient to execute a no-op instead.

If possible, the system should either:

(1) run these callbacks in the right order
(2) differentiate between the above condition and any more dangerous conditions

If no dangerous conditions exist, then the logging should just be removed.

wrap validator functions in a try/catch block

In the case that you have something like:

app.create.action('actionName', function (foo) {
  this.require(foo, "foo is required")
  this.require(foo.id, "foo ID is required")
});

or even

app.create.action('actionName', function (foo) {
  this.require(foo.id, "foo ID is required")
});

If foo is undefined, then the exception should be caught and the action would not be dispatched.

async methods from stores

We should not give store's action callbacks access to an @actions object.

But, async operations created via store methods could be controlled, and their callbacks could be executed in a second private context, which would have an @actions object present. (@action would likely also have to be wired up to match the calling action as show below, or variable references will need to be passed in some other way, whatever is more reliable)

Below is a store which has an @ajax method which copy's jquery's syntax, but binds the callbacks to the the special async context. A more generic @async method might also be appropriate. @promise is not much of a stretch from there. The only difference is that .done() or in fact any of the .then() calls would have access to the @update or other state altering methods. The .done() would have to issue an action in order to effect a change in state.

It is still the case that for larger applications, adapters should be used for async tasks like networking (especially if any sort of cooperation, throttling, queueing, etc is desired), but for simple stores that only make GET calls, this provides a much more succinct definition. It is effectively an 'inline adapter'.

app.createStore 'someStore',
  intialize: -> 
    @update
      baz: 1

  privateMethod: (foo, bar) -> 
    foo + bar

  api:
    publicMethod: (foo) -> 
      @get(foo)

  actions:
    'someAction': -> 
      @update(baz: @action.baz)

    'otherAction': ->
      @ajax
        url: "/api/0/foos/#{@action.fooId}"
        type: 'GET'
        success: (response) -> 
          @actions.newFoo(@action.fooId, response)
        error: (error) -> 
          @actions.fooError(@action.fooId, error)

    'newFoo': -> 
      @update(@action.fooId, @action.foo)

    'fooError': ->
      @update(@action.fooId, {type: 'error', timestamp: new Date().getTime()})

Future Action Args Syntax

Problem Statement

Packaging action data onto an object provides 'named arugments' but leads to a bunch of boilerplate whenever you want to process an action as you peel the arguments back off.

Actions that do not pass data (somewhat an anti pattern, but not unreasonable) are ridiculous as they have no payload.

Trying to use the packager function as syntactic validation is cumbersome and unpredictable.

Proposal 1 (Will)

  • Action args are simply positional arguments passed straight from the action caller to the action callbacks.
  • createAction (or equivalent) declarations are only required if multiple stores listen to the same action.
  • createAction's function is simply a validation function on the syntactic correctness of the passed arguments. It is therefore basically argTypes to React's propTypes.
app.create.store 'foos', 
  stores: ['bars']
  actions: 
    'foo.create': (fooData) -> 
      @update(fooData.id, fooData)
    'bar.setId': (barId) -> 
      @update(fooId: @stores.bar.getDefaultFooId())

app.create.store 'bars', 
  actions: 
    'bar.setId': (barId) -> @update(barId: barId)

app.create.action 'bar.setId', (barId) -> 
  @isNumber(barId)

given these stores and actions:

app.actions.foo.create( {id: 12, baz: 42} ) # fires action

app.actions.foo.create( {id: null, baz: 'bloop'} ) # still fires action, store might blow up.  

app.actions.bar.setBarId(12) # fires action

app.actions.bar.setBarId(null) # doesn't fire action, logs an error

more info in action validation messages

Given an action like so:

app.create.action 'actionName', (arg1, arg2) -> 
  @require _.isString(arg1), "arg1 must be a string"

and a call like so:

> app.actions.actionName(null, null)
'viewName' sent the action 'actionName' with args [null, null] but validation failed with messages: 
      - arg1 must be a string
and the action was not dispatched.  
[storeName1,  storeName2] subscribe to this action

It would be nice if the action validator messsages spat out as much context as they can about what was going on in the state of the system when the action failed.

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.