hopper
Version:
An interpreter for the Grace programming language
488 lines (397 loc) • 12.8 kB
JavaScript
// A Promise-like implementation of asynchronous tasks. Tasks are compatible
// with Promise's 'thenable' definition, but are not compliant with the Promises
// specification.
;
var asap, timer, util;
require("setimmediate");
asap = require("asap");
util = require("./util");
timer = Date.now();
function DeferralError(message) {
var error;
if (message !== undefined) {
this.message = message;
}
error = new TypeError(this.message);
error.name = this.name;
this.stack = error.stack;
}
util.inherits(DeferralError, TypeError);
DeferralError.prototype.name = "DeferralError";
DeferralError.prototype.message = "A purely asynchronous task cannot be forced";
function InterruptError(message) {
var error;
if (message !== undefined) {
this.message = message;
}
error = new Error(this.message);
error.name = this.name;
this.stack = error.stack;
}
util.inherits(InterruptError, Error);
InterruptError.prototype.name = "InterruptError";
InterruptError.prototype.message = "A task was stopped before it completed";
// Pump the task dependency queue and remove both queues when done.
function pump(task, list, arg) {
task.isPending = false;
while (list.length > 0) {
list.shift()(arg);
}
delete task.onFulfilled;
delete task.onRejected;
}
// Handle passing the outcome of a task to the next.
function completion(task, fresh, next, passthrough, resolve, reject) {
return function (result) {
// Regardless of whether or not the fresh task still depended on the outcome
// of the previous task, it can't be waiting on it any longer (because it's
// finished). This property may be reinstated by the call to 'next' below,
// as the fresh task can now depend on the result of one of the functions
// passed to 'next' (or 'now').
delete fresh.waitingOn;
// Due to the presence of 'stop', the fresh task may have already completed
// before the task it depended on did. In this case, don't perform the next
// action.
if (fresh.isPending) {
if (typeof next === "function") {
try {
result = next.call(task.context, result);
} catch (error) {
reject(error);
return;
}
resolve(result);
} else {
passthrough(result);
}
}
};
}
// new Task(context : Object = null, func : (Object -> (), Error -> ()) -> ())
// Build a new task, running the given function with a resolve and reject
// callback, optionally in the given context.
function Task(context, func) {
var self = this;
if (arguments.length < 2) {
func = context;
context = null;
}
this.isPending = true;
this.context = context;
this.onFulfilled = [];
this.onRejected = [];
func.call(context, function (value) {
if (self.isPending) {
self.value = value;
pump(self, self.onFulfilled, value);
}
}, function (reason) {
if (self.isPending) {
self.reason = reason;
pump(self, self.onRejected, reason);
}
}, this);
}
function then(task, run) {
return new Task(task.context, function (resolve, reject, fresh) {
// A task can be waiting on one of two tasks: either it is waiting for a
// value to be produced by the original task the 'then' method was called
// on, or it is waiting for the task created by the function passed to
// 'then'. In this case, it is waiting for the former. Note that the
// original task may have already completed, in which case it will switch to
// waiting on the latter.
fresh.waitingOn = task;
run.call(task, function (value, force) {
if (value === fresh) {
throw new TypeError("A task must not resolve to itself");
}
if (value instanceof Task) {
if (value.isPending) {
// The original task is done, and the function that ran as a result
// has produced a new task, meaning the fresh task now depends on that
// instead. Note that we cannot get here if the fresh task is stopped
// before the original task completes.
fresh.waitingOn = value;
value[force ? "now" : "then"](resolve).then(null, reject);
} else if (util.owns(value, "value")) {
resolve(value.value);
} else {
reject(value.reason);
}
} else {
resolve(value);
}
}, reject, fresh);
});
}
Task.prototype.then = function (onFulfilled, onRejected) {
return then(this, function (res, reject, fresh) {
var deferred, self;
self = this;
deferred = util.once(function (force) {
delete fresh.deferred;
function resolve(value) {
res(value, force);
}
function fulfiller() {
return completion(self, fresh, onFulfilled, resolve, resolve, reject);
}
function rejecter() {
return completion(self, fresh, onRejected, reject, resolve, reject);
}
if (force && util.owns(self, "deferred")) {
self.deferred(force);
}
if (self.isPending) {
self.onFulfilled.push(fulfiller());
self.onRejected.push(rejecter());
} else if (util.owns(self, "value")) {
fulfiller()(self.value);
} else {
rejecter()(self.reason);
}
});
fresh.deferred = deferred;
if (Date.now() - timer > 10) {
setImmediate(function () {
timer = Date.now();
deferred();
});
} else {
asap(deferred);
}
});
};
// Execute the callbacks immediately if this task is complete. If this task is
// still pending, attempt to force the task to finish. If the task cannot be
// forced, then the resulting task is rejected with a DeferralError.
Task.prototype.now = function (onFulfilled, onRejected) {
if (util.owns(this, "deferred")) {
this.deferred(true);
}
if (this.isPending) {
return Task.reject(new DeferralError());
}
return then(this, function (res, reject, fresh) {
function resolve(value) {
res(value, true);
}
if (util.owns(this, "value")) {
completion(this,
fresh, onFulfilled, resolve, resolve, reject)(this.value);
} else {
completion(this, fresh, onRejected, reject, resolve, reject)(this.reason);
}
});
};
Task.prototype.callback = function (callback) {
return this.then(callback && function (value) {
callback.call(this, null, value);
}, callback);
};
Task.prototype.bind = function (context) {
var task = this.then(util.id);
task.context = context;
return task;
};
// Halt the execution of this task and tasks it depends on. If the task has not
// already completed, called this method causes this task and its dependencies
// to be rejected with an InterruptError. This method does not guarantee an
// immediate stop, as tasks may yield outside of the internal task machinery,
// and their resumption may have side-effects before completing their
// surrounding task.
//
// Note that tasks that have been spawned by the task dependency chain that are
// not included in the dependency chain (ie concurrent executions) will not be
// stopped by this method. They must be managed separately.
Task.prototype.stop = function () {
var dependency;
if (!this.isPending) {
// If the task is already completed, stopping has no effect.
return;
}
// It's possible to be waiting on a task that isn't pending, when this task
// is being synchronously stopped after the task it depends on has completed,
// but before the asynchronous chaining can occur. If this is the case, we'll
// pump now, setting this task to a completed state, and when the asynchronous
// completion runs in the future the waitingOn dependency will be deleted but
// no other action will be taken.
if (this.waitingOn !== undefined && this.waitingOn.isPending) {
// The rejection of this task will occur once the dependency chain is also
// rejected.
dependency = this.waitingOn;
asap(function () {
dependency.stop();
});
} else {
this.reason = new InterruptError();
pump(this, this.onRejected, this.reason);
}
};
// A utility method to produce a function that will stop this task when called.
Task.prototype.stopify = function () {
var self = this;
return function () {
return self.stop();
};
};
Task.resolve = function (context, value) {
if (arguments.length < 2) {
value = context;
context = null;
}
if (value instanceof Task) {
return value;
}
return new Task(context, function (resolve) {
resolve(value);
});
};
Task.reject = function (context, reason) {
if (arguments.length < 2) {
reason = context;
context = null;
}
return new Task(context, function (resolve, reject) {
reject(reason);
});
};
Task.never = function (context) {
if (arguments.length < 1) {
context = null;
}
return new Task(context, function () {
return;
});
};
// each(context : Object = null,
// lists+ : [T], action : T+ -> Task<U>) -> Task<[U]>
// Run an asynchronous action over lists of arguments in order, chaining each
// non-undefined result of the action into a list. Multiple lists must have
// matching lengths. The context must not be an array, otherwise it must be
// bound manually.
Task.each = function (context, first) {
var action, i, j, l, length, part, parts, results;
function run(k, task) {
if (k === length) {
return task.then(function () {
return results;
});
}
return run(k + 1, task.then(function () {
return action.apply(this, parts[k]);
}).then(function (value) {
if (value !== undefined) {
results.push(value);
}
}));
}
if (util.isArray(context) ||
typeof context === "number" || typeof context === "string") {
first = context;
context = null;
} else {
Array.prototype.shift.call(arguments);
}
results = [];
parts = [];
l = arguments.length - 1;
action = arguments[l];
if (typeof first === "number") {
length = first;
for (i = 0; i < length; i += 1) {
parts.push([i]);
}
} else {
length = first.length;
for (i = 0; i < l; i += 1) {
if (arguments[i].length !== length) {
throw new TypeError("Mismatched list lengths");
}
}
for (i = 0; i < length; i += 1) {
part = [];
for (j = 0; j < l; j += 1) {
part.push(arguments[j][i]);
}
part.push(i);
parts.push(part);
}
}
// This is here to allow the list length check above to occur first.
if (length === 0) {
return Task.resolve(context, []);
}
return run(0, Task.resolve(context, null));
};
// Translate a function that may return a task into a function that takes a
// callback. If the function throws, the error is bundled into the callback.
// The resulting function returns another function which will call 'stop' on the
// underlying task.
Task.callbackify = function (func) {
return function () {
var args, callback, task;
args = util.slice(arguments);
callback = args.pop();
try {
task = func.apply(this, args);
} catch (reason) {
callback(reason);
return function () {
return false;
};
}
return Task.resolve(task).callback(callback).stopify();
};
};
// Translate a function that takes a callback into a function that returns a
// Task. If the function throws, the task automatically rejects.
Task.taskify = function (context, func) {
if (arguments.length < 2) {
func = context;
context = null;
}
return function () {
var args, self;
self = this;
args = util.slice(arguments);
return new Task(context, function (resolve, reject) {
args.push(function (reason, value) {
if (reason !== null) {
reject(reason);
} else {
resolve(value);
}
});
try {
func.apply(self, args);
} catch (reason) {
reject(reason);
}
});
};
};
// An abstract constructor that includes helpers for maintaining the state of
// the 'this' context while performing task operations.
function Async() {
return this;
}
// Resolve to a task with this object as the context.
Async.prototype.resolve = function (value) {
return Task.resolve(this, value);
};
Async.prototype.reject = function (reason) {
return Task.reject(this, reason);
};
Async.prototype.task = function (action) {
return Task.resolve(this, null).then(function () {
return action.call(this);
});
};
Async.prototype.each = function () {
return Task.each.apply(Task, [this].concat(util.slice(arguments)));
};
Task.DeferralError = DeferralError;
Task.InterruptError = InterruptError;
Task.Async = Async;
module.exports = Task;