state-trooper
Version:
Application State Manager
331 lines (290 loc) • 8.06 kB
JavaScript
"use strict";
var buffers = require("./buffers");
var dispatch = require("./dispatch");
var MAX_DIRTY = 64;
var MAX_QUEUE_SIZE = 1024;
var CLOSED = null;
var Box = function(value) {
this.value = value;
};
var PutBox = function(handler, value) {
this.handler = handler;
this.value = value;
};
var Channel = function(takes, puts, buf, xform) {
this.buf = buf;
this.xform = xform;
this.takes = takes;
this.puts = puts;
this.dirty_takes = 0;
this.dirty_puts = 0;
this.closed = false;
};
function isReduced(v) {
return v && v["@@transducer/reduced"];
}
function schedule(f, v) {
dispatch.run(function() {
f(v);
});
}
Channel.prototype._put = function(value, handler) {
if (value === CLOSED) {
throw new Error("Cannot put CLOSED on a channel.");
}
// TODO: I'm not sure how this can happen, because the operations
// are registered in 1 tick, and the only way for this to be inactive
// is for a previous operation in the same alt to have returned
// immediately, which would have short-circuited to prevent this to
// be ever register anyway. The same thing goes for the active check
// in "_take".
if (!handler.is_active()) {
return null;
}
if (this.closed) {
handler.commit();
return new Box(false);
}
var taker, callback;
// Soak the value through the buffer first, even if there is a
// pending taker. This way the step function has a chance to act on the
// value.
if (this.buf && !this.buf.is_full()) {
handler.commit();
var done = isReduced(this.xform["@@transducer/step"](this.buf, value));
while (true) {
if (this.buf.count() === 0) {
break;
}
taker = this.takes.pop();
if (taker === buffers.EMPTY) {
break;
}
if (taker.is_active()) {
value = this.buf.remove();
callback = taker.commit();
schedule(callback, value);
}
}
if (done) {
this.close();
}
return new Box(true);
}
// Either the buffer is full, in which case there won't be any
// pending takes, or we don't have a buffer, in which case this loop
// fulfills the first of them that is active (note that we don't
// have to worry about transducers here since we require a buffer
// for that).
while (true) {
taker = this.takes.pop();
if (taker === buffers.EMPTY) {
break;
}
if (taker.is_active()) {
handler.commit();
callback = taker.commit();
schedule(callback, value);
return new Box(true);
}
}
// No buffer, full buffer, no pending takes. Queue this put now if blockable.
if (this.dirty_puts > MAX_DIRTY) {
this.puts.cleanup(function(putter) {
return putter.handler.is_active();
});
this.dirty_puts = 0;
} else {
this.dirty_puts ++;
}
if (handler.is_blockable()) {
if (this.puts.length >= MAX_QUEUE_SIZE) {
throw new Error("No more than " + MAX_QUEUE_SIZE + " pending puts are allowed on a single channel.");
}
this.puts.unbounded_unshift(new PutBox(handler, value));
}
return null;
};
Channel.prototype._take = function(handler) {
if (!handler.is_active()) {
return null;
}
var putter, put_handler, callback, value;
if (this.buf && this.buf.count() > 0) {
handler.commit();
value = this.buf.remove();
// We need to check pending puts here, other wise they won't
// be able to proceed until their number reaches MAX_DIRTY
while (true) {
if (this.buf.is_full()) {
break;
}
putter = this.puts.pop();
if (putter === buffers.EMPTY) {
break;
}
put_handler = putter.handler;
if (put_handler.is_active()) {
callback = put_handler.commit();
if (callback) {
schedule(callback, true);
}
if (isReduced(this.xform["@@transducer/step"](this.buf, putter.value))) {
this.close();
}
}
}
return new Box(value);
}
// Either the buffer is empty, in which case there won't be any
// pending puts, or we don't have a buffer, in which case this loop
// fulfills the first of them that is active (note that we don't
// have to worry about transducers here since we require a buffer
// for that).
while (true) {
putter = this.puts.pop();
value = putter.value;
if (putter === buffers.EMPTY) {
break;
}
put_handler = putter.handler;
if (put_handler.is_active()) {
handler.commit();
callback = put_handler.commit();
if (callback) {
schedule(callback, true);
}
return new Box(value);
}
}
if (this.closed) {
handler.commit();
return new Box(CLOSED);
}
// No buffer, empty buffer, no pending puts. Queue this take now if blockable.
if (this.dirty_takes > MAX_DIRTY) {
this.takes.cleanup(function(handler) {
return handler.is_active();
});
this.dirty_takes = 0;
} else {
this.dirty_takes ++;
}
if (handler.is_blockable()) {
if (this.takes.length >= MAX_QUEUE_SIZE) {
throw new Error("No more than " + MAX_QUEUE_SIZE + " pending takes are allowed on a single channel.");
}
this.takes.unbounded_unshift(handler);
}
return null;
};
Channel.prototype.close = function() {
if (this.closed) {
return;
}
this.closed = true;
// TODO: Duplicate code. Make a "_flush" function or something
if (this.buf) {
this.xform["@@transducer/result"](this.buf);
while (true) {
if (this.buf.count() === 0) {
break;
}
taker = this.takes.pop();
if (taker === buffers.EMPTY) {
break;
}
if (taker.is_active()) {
callback = taker.commit();
var value = this.buf.remove();
schedule(callback, value);
}
}
}
while (true) {
var taker = this.takes.pop();
if (taker === buffers.EMPTY) {
break;
}
if (taker.is_active()) {
var callback = taker.commit();
schedule(callback, CLOSED);
}
}
while (true) {
var putter = this.puts.pop();
if (putter === buffers.EMPTY) {
break;
}
if (putter.handler.is_active()) {
var put_callback = putter.handler.commit();
if (put_callback) {
schedule(put_callback, false);
}
}
}
};
Channel.prototype.is_closed = function() {
return this.closed;
};
function defaultHandler(e) {
console.log('error in channel transformer', e.stack);
return CLOSED;
}
function handleEx(buf, exHandler, e) {
var def = (exHandler || defaultHandler)(e);
if (def !== CLOSED) {
buf.add(def);
}
return buf;
}
// The base transformer object to use with transducers
function AddTransformer() {
}
AddTransformer.prototype["@@transducer/init"] = function() {
throw new Error('init not available');
};
AddTransformer.prototype["@@transducer/result"] = function(v) {
return v;
};
AddTransformer.prototype["@@transducer/step"] = function(buffer, input) {
buffer.add(input);
return buffer;
};
function handleException(exHandler) {
return function(xform) {
return {
"@@transducer/step": function(buffer, input) {
try {
return xform["@@transducer/step"](buffer, input);
} catch (e) {
return handleEx(buffer, exHandler, e);
}
},
"@@transducer/result": function(buffer) {
try {
return xform["@@transducer/result"](buffer);
} catch (e) {
return handleEx(buffer, exHandler, e);
}
}
};
};
}
// XXX: This is inconsistent. We should either call the reducing
// function xform, or call the transducer xform, not both
exports.chan = function(buf, xform, exHandler) {
if (xform) {
if (!buf) {
throw new Error("Only buffered channels can use transducers");
}
xform = xform(new AddTransformer());
} else {
xform = new AddTransformer();
}
xform = handleException(exHandler)(xform);
return new Channel(buffers.ring(32), buffers.ring(32), buf, xform);
};
exports.Box = Box;
exports.Channel = Channel;
exports.CLOSED = CLOSED;