Async data pipelines for client-side JavaScript

Preview:

DESCRIPTION

Patterns like data pipelines, queuing and multiplexing are familiar to backend developers working on distributed and high-traffic systems. Projects such as Node.js or ZeroMQ make the concepts of streams, queues and pipelines first-order primitives that allow you to compose software in an organic and declarative way. In this talk, I will attempt to bring together these patterns and principles and explore how they can be applied to everyday client-side JavaScript programming.

Citation preview

ASYNC PIPELINES in client-side JavaScript

(an experiment)

ismael celis . new-bamboo.co.uk . github.com/ismasan

V/Capp.AppView = Backbone.View.extend({ initialize: function () { ... }, render: function () { ... },! addOne: function (todo) { ... }, addAll: function () { ... }, filterOne: function (todo) { todo.trigger('visible'); }, filterAll: function () { ... }!}

Mapp.Todo = Backbone.Model.extend({! defaults: { title: '', completed: false }, toggle: function () { this.save({ completed: !this.get('completed') }); }, processAndSave: function (data, callback) { this.set(data) var self = this this.save(function () { callback() self.trigger('processed') Todo.trigger('processed', self) }) }});

T<h1>{{ count }} productos encontrados</h1>!<ul>{{#products}} <li><a href="{{ href }}">{{name}}</a></li>{{/products}}</ul>

+ verbs - nouns

How my JS apps should be built

How my JS apps end up being built

...

BASIC OBJECTvar Person = BasicObject.extend({ initialize: function (name) { this.name = name }, talk: function () { alert("What's up!") }}) var Programmer = Person.extend({ talk: function () { alert("Go away, I'm busy writing code!") }})

STRUCT

var mario = new Struct({name: 'Mario'})!

mario.set('name', 'Luigi') // triggers `change:name` event!

mario.get('name') // “Luigi"!

mario.uid() // "uid13983601141350.9170819637365639"

var p1 = new Pipe()var p2 = new Pipe()var p3 = new Pipe()!

p1.pipe(p2).pipe(p3)!

p3.on('add', function (struct) { console.log('p3 received data!', struct)})!

p1.add(mario) // forwards mario to other pipes downstream, // including p2 and then p3

PIPE

PIPE

pipe(anotherPipe) !

add(struct) !

remove(struct)

PIPE

!

add(struct) !

filter(struct, filterPromise) !

_add(struct, addPromise) !

_formwardAdd(struct)

PIPE + FILTER

var MarioFilter = Pipe.extend({!

filter: function (struct, filterPromise) { if(struct.get('name') == 'Mario') filterPromise.resolve(struct) else filterPromise.reject(struct) }!

})

PIPE + FILTER

var filter = new MarioFilter()!

filter.pipe(p2).pipe(p3)!

filter.add(new Struct({name: 'Mario'})) // p2 and p3 get struct added!

filter.add(new Struct({name: 'Luigi'})) // filter is rejected. p2 and p3 DO NOT get struct added

PIPE + FILTER + AJAXvar MarioFilter = Pipe.extend({ filter: function (struct, filterPromise) { $.get( 'http://some.api.com/valid_mario', {name: struct.get('name')} ).then( function () { filterPromise.resolve(struct) }, // success function () { filterPromise.reject(struct) } // error ) } })

PIPE

api.jquery.com/jQuery.when/

$.when( pipe1.add(struct), pipe2.add(struct), pipe3.add(struct) ).then(...)

INDEX

var Index = Pipe.extend({ initialize: function () { this._index = {} this._list = [] }, !

…})

INDEXvar Index = Pipe.extend({ … _add: function (struct, addPromise) { if(! this._index[struct.uid()]) { this._index[struct.uid()] = struct this._list.push(struct) addPromise.resolve(struct) } }})

INDEXvar index = new Index()index.add(mario)index.add(luigi)!

index.pipe(another_pipe) // ‘another_pipe’ will get mario and luigi added to it now!

index.add(princess) // ‘another_pipe’ gets princess added to it.

INDEX

index.add(mario) // forwards to other pipes!

mario.set(age: 30)!

index.add(mario) // does not forward.

CAPPED INDEXvar CappedIndex = Index.extend({!

limit: 10,!

_add: function (struct, promise) { // remove first if limit reached if(this._list.length > this.limit - 1) this.remove(this._list[0])

// add next return Index.prototype._add.call(this, struct, promise) }!

})

DEVICES

CHOKE POINT

var p1 = new Pipe()var p2 = new SomeAjaxPipe()var results = new Pipe()var choke = new ChokePoint(p1, p2)!

choke.pipe(results)!

choke.add(struct)

PIPELINEvar p1 = new Pipe()var p2 = new SomeAjaxPipe()var results = new Pipe()var pipeline = new LeakyPipeline(p1, p2)!

pipeline.add(struct) // will forward struct to p1, p2 and finally pipe on to results pipe

FAN INvar in1 = new Pipe() var in2 = new Pipe()var results = new Pipe()var fanIn = new Ventilator([in1, in2])!

fanIn.pipe(results)!

in1.add(struct) // forwards struct to results

FAN OUT

var out1 = new Pipe() var out2 = new Pipe()!

var fanOut = new Ventilator(null, [out1, out2])!

fanOut.add(struct) // forwards struct to out1 and out2

MANY-TO-MANY

var ventilator = new Ventilator( [in1, in2], [out1, out2])!

in1.add(struct) // forwards struct to out1 and out2in2.add(struct) // forwards struct to out1 and out2

ROUTERvar p1 = new Pipe()var p2 = new Pipe()var default= new Pipe()!

var router = new Router()!

router .route(p1, function (struct, promise) { if(struct.get('name') == 'Joe') promise.resolve() else promise.reject() }) .route(p2, function (struct, promise) { if(struct.get('name') == 'Jane') promise.resolve() else promise.reject() }) .default(default)

ROUTERrouter.add(new Struct({name: 'Joe'})) // forwards struct to p1!

router.add(new Struct({name: 'Jane'})) // forwards struct to p2!

router.add(new Struct({name: 'Paul'})) // forwards struct to default!

router.pipe(other) // pipes all data to `other`.

CUSTOM DEVICEvar MyCustomDevice = Pipe.extend({!

filter: function (struct, filterPromise) {...},!

_add: function (struct, addPromise) {...},!

_remove: function (struct, removePromise) {...},!

_forwardAdd: function (struct) {...},!

_forwardRemove: function (struct) {...}})

CUSTOM DEVICE

Throttle, Buffer, Aggregator, Counter, Identity Map, State Machine

REPOSITORY// users.pipe(results).get('http://some.api.com/users')var Users = Pipe.extend({!

// Get from remote API get: function (url) { var self = this!

$.getJSON(url).then(function (data) { data.forEach(function (user) { self.add(user) }) }) return this }})

REPOSITORYvar Users = Pipe.extend({! // A repository can have its own index initialize: function () { this.__index = new Index(UserStruct) // pipe index to itself so repository can pipe to other pipes transparently this.__index.pipe(this) },! // Get from remote API get: function (url) { var self = this! $.getJSON(url).then(function (data) { data.forEach(function (user) { self.__index.add(user) }) }) return this }})

REPOSITORYvar Users = Pipe.extend({!

_add: function (struct, addPromise) { // send data to server $.ajax(this.url, { type: 'post', dataType: 'json', data: struct.attributes }).then(function (data) { struct.set(data) // update struct with data coming from server

addPromise.resolve(struct) }) }!

})

FRAMEWORK ?

VIEWvar View = Pipe.extend({ initialize: function ($e) { ... this._children = {} }, _add: function (item, promise) { // create and index child view this._children[item.uid()] = new ChildElement(this.$itemTemplate) promise.resolve(item) }, _remove: function (item, promise) { // unbind, destroy and de-index child-view var child = this._children[item.uid()].destroy() delete this._children[item.uid()] promise.resolve(item) } })

VIEW

rivetsjs.com

<div id="items"> <ul data-item> <li data-text="item.name"></li> </ul> </div>

VIEW

var view = new View($('#items'))!

var mario = new Struct({name: 'Mario'})!

view.add(mario)

CAPPED VIEW

function cappedView($e, limit) { var index = new CappedIndex(limit) var view = new View($e) index.pipe(view) return index}

APP

var timeline = cappedView($('#timeline'), 10)!

var socket = new SocketRepository('ws://some.server.com')!

socket.pipe(timeline)

gist.github.com/ismasan/5848735

github.com/ismasan/plumber.js

reactive-extensions.github.io/RxJS/

ismael celis . new-bamboo.co.uk . github.com/ismasan

Recommended