jspipe
Version:
JS/Pipe - coordinating asynchronous code without callbacks or chained functions
510 lines (432 loc) • 13.4 kB
JavaScript
(function(global) {
if (global.JSPipe) { return; }
// JSPipe is free software distributed under the terms of the MIT license reproduced here.
// JSPipe may be used for any purpose, including commercial purposes, at absolutely no cost.
// No paperwork, no royalties, no GNU-like "copyleft" restrictions, either.
// Just download it and use it.
// Copyright (c) 2013 Joubert Nel
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
var sentinel = '|Ω|';
function isGeneratorFunction(fn) {
return true;
// Don't do real checking yet, because it fails
// in Firefox when using traceur for simulating
// generators.
// var fn = Function.isGenerator;
// return fn && fn.call(x);
}
/**
* Kick off a job. A job runs concurrently with other jobs.
*
* To communicate and synchronize with another job, communicate via a
* Pipe.
*/
function job(fn, args) {
var generator,
next;
if (isGeneratorFunction(fn)) {
generator = fn.apply(fn, args);
next = function(data) {
var nextItem = generator.next(data),
done = nextItem.done,
value = nextItem.value,
res;
if (done) {
res = null;
} else {
res = value ? value(next) : next();
}
return res;
};
next();
} else {
throw new TypeError('function must be a generator function, i.e. function* () {...} ');
}
}
/**
* A pipe provides a way for two jobs to communicate data + synchronize their execution.
*
* One job can send data by calling "yield pipe.put(data)" and another job can
* receive data by calling "yield pipe.get()".
*/
function Pipe() {
this.syncing = false;
this.inbox = [];
this.outbox = [];
this.isOpen = true;
}
Pipe.prototype.close = function() {
this.isOpen = false;
};
/**
* Call "yield pipe.put(data)" from a job (the sender) to put data in the pipe.
*
* The put method will then try to rendezvous with a receiver job, if any.
* If there is no receiver waiting for data, the sender will pause until another
* job calls "yield pipe.get()", which will then trigger a rendezvous.
*/
Pipe.prototype.put = function(data) {
var self = this;
return function(resume) {
self.inbox.push(data, resume);
// Try to rendezvous with a receiver
self._rendezvous();
};
};
Pipe.prototype.waiting = function() {
return this.outbox.length;
};
/**
* Call "yield pipe.get()" from a job (the receiver) to get data from the pipe.
*
* The get method will then try to rendezvous with a sender job, if any.
* If there is no sender waiting for the data it sent to be delivered, the receiver will
* pause until another job calls "yield pipe.put(data)", which will then trigger
* a rendezvous.
*/
Pipe.prototype.get = function() {
var self = this;
return function(resume) {
self.outbox.push(resume);
// Try to rendezvous with sender
self._rendezvous();
};
};
Pipe.prototype.send = function(message) {
this.put(message)();
};
/**
* A pipe is a rendezvous point for two otherwise independently executing jobs.
* Such communication + synchronization on a pipe requires a sender and receiver.
*
* A job sends data to a pipe using "yield pipe.put(data)".
* Another job receives data from a pipe using "yield pipe.get()".
*
* Once both a sender job and a receiver job are waiting on the pipe,
* the _rendezvous method transfers the data in the pipe to the receiver and consequently
* synchronizes the two waiting jobs.
*
* Once synchronized, the two jobs continue execution.
*/
Pipe.prototype._rendezvous = function() {
var syncing = this.syncing,
inbox = this.inbox,
outbox = this.outbox,
data,
notify,
send,
receipt,
senderWaiting,
receiverWaiting;
if (!syncing) {
this.syncing = true;
while ((senderWaiting = inbox.length > 0) &&
(receiverWaiting = outbox.length > 0)) {
// Get the data that the sender job put in the pipe
data = inbox.shift();
// Get the method to notify the sender once the data has been
// delivered to the receiver job
notify = inbox.shift();
// Get the method used to send the data to the receiver job.
send = outbox.shift();
// Send the data
receipt = send(data);
// Notify the sender that the data has been sent
if (notify) {
notify(receipt);
}
}
this.syncing = false;
}
};
function EventPipe(el, type, handler) {
// super
Pipe.call(this);
this._el = el;
this._type = type;
this._handler = handler;
el.addEventListener(type, handler);
}
EventPipe.prototype = Object.create(Pipe.prototype);
EventPipe.prototype.close = function() {
this._el.removeEventListener(this._type, this._handler);
delete this._el;
delete this._type;
delete this._handler;
// super
Pipe.prototype.close.call(this);
};
///
/// Pipe producers
///
function timeout(ms, interruptor) {
// TODO: model timeout as a process
var output = new Pipe();
setTimeout(function() {
job(wrapGenerator.mark(function() {
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
$ctx.next = 2;
return output.put(ms);
case 2:
case "end":
return $ctx.stop();
}
}, this);
}));
}, ms);
return output;
}
function listen(el, type, preventDefault) {
var handler = function(e) {
if (preventDefault) {
e.preventDefault();
}
output.send(e);
};
var output = new EventPipe(el, type, handler);
return output;
}
function jsonp(url, id) {
var output = new Pipe();
$.getJSON(url, function(data) {
job(wrapGenerator.mark(function() {
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
$ctx.next = 2;
return output.put({ data: data,
id: id })
case 2:
case "end":
return $ctx.stop();
}
}, this);
}));
});
return output;
}
function lazyseq(count, fn) {
var output = new Pipe();
job(wrapGenerator.mark(function() {
var data, i;
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
i = 0;
case 1:
if (!(0 < count--)) {
$ctx.next = 8;
break;
}
data = fn(i);
$ctx.next = 5;
return output.put(data);
case 5:
i++;
$ctx.next = 1;
break;
case 8:
$ctx.next = 10;
return output.put(sentinel);
case 10:
output.close();
case 11:
case "end":
return $ctx.stop();
}
}, this);
}));
return output;
}
///
/// Pipe transformers
///
function unique(pipe) {
var output = new Pipe();
job(wrapGenerator.mark(function() {
var isFirstData, data, lastData;
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
isFirstData = true;
case 1:
if (!pipe.isOpen) {
$ctx.next = 12;
break;
}
$ctx.next = 4;
return pipe.get();
case 4:
data = $ctx.sent;
if (!(isFirstData || data !== lastData)) {
$ctx.next = 9;
break;
}
$ctx.next = 8;
return output.put(data);
case 8:
isFirstData = false;
case 9:
lastData = data;
$ctx.next = 1;
break;
case 12:
output.close();
case 13:
case "end":
return $ctx.stop();
}
}, this);
}));
return output;
}
function pace(ms, pipe) {
var output = new Pipe();
job(wrapGenerator.mark(function() {
var timeoutId, data, send;
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
send = function(data) { output.send(data); };
case 1:
if (!pipe.isOpen) {
$ctx.next = 9;
break;
}
$ctx.next = 4;
return pipe.get();
case 4:
data = $ctx.sent;
clearTimeout(timeoutId);
timeoutId = setTimeout(send.bind(output, data), ms);
$ctx.next = 1;
break;
case 9:
output.close();
case 10:
case "end":
return $ctx.stop();
}
}, this);
}));
return output;
}
function delay(pipe, ms) {
var output = new Pipe();
job(wrapGenerator.mark(function() {
var data;
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
if (!pipe.isOpen) {
$ctx.next = 9;
break;
}
$ctx.next = 3;
return timeout(ms).get();
case 3:
$ctx.next = 5;
return pipe.get();
case 5:
data = $ctx.sent;
output.send(data);
$ctx.next = 0;
break;
case 9:
case "end":
return $ctx.stop();
}
}, this);
}));
return output;
}
///
/// Pipe coordination
///
function select(cases) {
// TODO: consider rewriting as a sweetjs macro
var output = new Pipe(),
done = new Pipe(),
remaining = cases.length;
cases.forEach(function(item) {
job(wrapGenerator.mark(function() {
var pipe, response, data;
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
pipe = item.pipe, response = item.response;
$ctx.next = 3;
return pipe.get();
case 3:
data = $ctx.sent;
response(data);
$ctx.next = 7;
return done.put(true);
case 7:
case "end":
return $ctx.stop();
}
}, this);
}));
});
job(wrapGenerator.mark(function() {
return wrapGenerator(function($ctx) {
while (1) switch ($ctx.next) {
case 0:
if (!(remaining > 0)) {
$ctx.next = 6;
break;
}
$ctx.next = 3;
return done.get();
case 3:
remaining = remaining - 1;
$ctx.next = 0;
break;
case 6:
$ctx.next = 8;
return output.put(sentinel);
case 8:
case "end":
return $ctx.stop();
}
}, this);
}));
return output;
}
function range() {
// TODO: consider writing as a sweetjs macro
throw 'Range has not been implemented yet.';
}
global.JSPipe = {
job: job,
Pipe: Pipe,
EventPipe: EventPipe,
timeout: timeout,
listen: listen,
jsonp: jsonp,
lazyseq: lazyseq,
unique: unique,
pace: pace,
delay: delay,
sentinel: sentinel,
select: select,
range: range
};
})(typeof global !== "undefined" ? global : this);