155 lines
4.3 KiB
JavaScript
155 lines
4.3 KiB
JavaScript
|
/* eslint consistent-this: 0, no-shadow:0, no-eq-null: 0, eqeqeq: 0, no-unused-vars: 0 */
|
||
|
|
||
|
// Support for asynchronous functions
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
var aFrom = require("es5-ext/array/from")
|
||
|
, objectMap = require("es5-ext/object/map")
|
||
|
, mixin = require("es5-ext/object/mixin")
|
||
|
, defineLength = require("es5-ext/function/_define-length")
|
||
|
, nextTick = require("next-tick");
|
||
|
|
||
|
var slice = Array.prototype.slice, apply = Function.prototype.apply, create = Object.create;
|
||
|
|
||
|
require("../lib/registered-extensions").async = function (tbi, conf) {
|
||
|
var waiting = create(null)
|
||
|
, cache = create(null)
|
||
|
, base = conf.memoized
|
||
|
, original = conf.original
|
||
|
, currentCallback
|
||
|
, currentContext
|
||
|
, currentArgs;
|
||
|
|
||
|
// Initial
|
||
|
conf.memoized = defineLength(function (arg) {
|
||
|
var args = arguments, last = args[args.length - 1];
|
||
|
if (typeof last === "function") {
|
||
|
currentCallback = last;
|
||
|
args = slice.call(args, 0, -1);
|
||
|
}
|
||
|
return base.apply(currentContext = this, currentArgs = args);
|
||
|
}, base);
|
||
|
try { mixin(conf.memoized, base); }
|
||
|
catch (ignore) {}
|
||
|
|
||
|
// From cache (sync)
|
||
|
conf.on("get", function (id) {
|
||
|
var cb, context, args;
|
||
|
if (!currentCallback) return;
|
||
|
|
||
|
// Unresolved
|
||
|
if (waiting[id]) {
|
||
|
if (typeof waiting[id] === "function") waiting[id] = [waiting[id], currentCallback];
|
||
|
else waiting[id].push(currentCallback);
|
||
|
currentCallback = null;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Resolved, assure next tick invocation
|
||
|
cb = currentCallback;
|
||
|
context = currentContext;
|
||
|
args = currentArgs;
|
||
|
currentCallback = currentContext = currentArgs = null;
|
||
|
nextTick(function () {
|
||
|
var data;
|
||
|
if (hasOwnProperty.call(cache, id)) {
|
||
|
data = cache[id];
|
||
|
conf.emit("getasync", id, args, context);
|
||
|
apply.call(cb, data.context, data.args);
|
||
|
} else {
|
||
|
// Purged in a meantime, we shouldn't rely on cached value, recall
|
||
|
currentCallback = cb;
|
||
|
currentContext = context;
|
||
|
currentArgs = args;
|
||
|
base.apply(context, args);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Not from cache
|
||
|
conf.original = function () {
|
||
|
var args, cb, origCb, result;
|
||
|
if (!currentCallback) return apply.call(original, this, arguments);
|
||
|
args = aFrom(arguments);
|
||
|
cb = function self(err) {
|
||
|
var cb, args, id = self.id;
|
||
|
if (id == null) {
|
||
|
// Shouldn't happen, means async callback was called sync way
|
||
|
nextTick(apply.bind(self, this, arguments));
|
||
|
return undefined;
|
||
|
}
|
||
|
delete self.id;
|
||
|
cb = waiting[id];
|
||
|
delete waiting[id];
|
||
|
if (!cb) {
|
||
|
// Already processed,
|
||
|
// outcome of race condition: asyncFn(1, cb), asyncFn.clear(), asyncFn(1, cb)
|
||
|
return undefined;
|
||
|
}
|
||
|
args = aFrom(arguments);
|
||
|
if (conf.has(id)) {
|
||
|
if (err) {
|
||
|
conf.delete(id);
|
||
|
} else {
|
||
|
cache[id] = { context: this, args: args };
|
||
|
conf.emit("setasync", id, typeof cb === "function" ? 1 : cb.length);
|
||
|
}
|
||
|
}
|
||
|
if (typeof cb === "function") {
|
||
|
result = apply.call(cb, this, args);
|
||
|
} else {
|
||
|
cb.forEach(function (cb) { result = apply.call(cb, this, args); }, this);
|
||
|
}
|
||
|
return result;
|
||
|
};
|
||
|
origCb = currentCallback;
|
||
|
currentCallback = currentContext = currentArgs = null;
|
||
|
args.push(cb);
|
||
|
result = apply.call(original, this, args);
|
||
|
cb.cb = origCb;
|
||
|
currentCallback = cb;
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
// After not from cache call
|
||
|
conf.on("set", function (id) {
|
||
|
if (!currentCallback) {
|
||
|
conf.delete(id);
|
||
|
return;
|
||
|
}
|
||
|
if (waiting[id]) {
|
||
|
// Race condition: asyncFn(1, cb), asyncFn.clear(), asyncFn(1, cb)
|
||
|
if (typeof waiting[id] === "function") waiting[id] = [waiting[id], currentCallback.cb];
|
||
|
else waiting[id].push(currentCallback.cb);
|
||
|
} else {
|
||
|
waiting[id] = currentCallback.cb;
|
||
|
}
|
||
|
delete currentCallback.cb;
|
||
|
currentCallback.id = id;
|
||
|
currentCallback = null;
|
||
|
});
|
||
|
|
||
|
// On delete
|
||
|
conf.on("delete", function (id) {
|
||
|
var result;
|
||
|
// If false, we don't have value yet, so we assume that intention is not
|
||
|
// to memoize this call. After value is obtained we don't cache it but
|
||
|
// gracefully pass to callback
|
||
|
if (hasOwnProperty.call(waiting, id)) return;
|
||
|
if (!cache[id]) return;
|
||
|
result = cache[id];
|
||
|
delete cache[id];
|
||
|
conf.emit("deleteasync", id, slice.call(result.args, 1));
|
||
|
});
|
||
|
|
||
|
// On clear
|
||
|
conf.on("clear", function () {
|
||
|
var oldCache = cache;
|
||
|
cache = create(null);
|
||
|
conf.emit(
|
||
|
"clearasync", objectMap(oldCache, function (data) { return slice.call(data.args, 1); })
|
||
|
);
|
||
|
});
|
||
|
};
|