You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
154 lines
4.3 KiB
154 lines
4.3 KiB
/* 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); }) |
|
); |
|
}); |
|
};
|
|
|