/* 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); }) ); }); };