Skip to content

Instantly share code, notes, and snippets.

@amb26
Created October 20, 2014 21:46
Show Gist options
  • Save amb26/cc967b0715fbf6a5f54e to your computer and use it in GitHub Desktop.
Save amb26/cc967b0715fbf6a5f54e to your computer and use it in GitHub Desktop.
Asynchrouwnous LifecycleManager
/*!
Lifecycle Manager
Copyright 2012 Antranig Basman
Licensed under the New BSD license. You may not use this file except in
compliance with this License.
You may obtain a copy of the License at
https://github.com/gpii/universal/LICENSE.txt
*/
"use strict";
var fluid = fluid || require("infusion");
var $ = fluid.registerNamespace("jQuery");
var gpii = fluid.registerNamespace("gpii");
(function () {
fluid.defaults("gpii.lifecycleManager", {
gradeNames: ["fluid.eventedComponent", "autoInit"],
components: {
variableResolver: {
type: "gpii.lifecycleManager.variableResolver"
},
nameResolver: {
type: "gpii.lifecycleManager.nameResolver"
}
},
members: {
activeSessions: {}
},
invokers: {
getActiveSessionTokens: {
funcName: "gpii.lifecycleManager.getActiveSessionTokens",
args: "{that}.activeSessions"
},
getSession: {
funcName: "gpii.lifecycleManager.getSession",
args: ["{that}.activeSessions", "{arguments}.0"] // token
},
stop: {
funcName: "gpii.lifecycleManager.stop",
args: ["{that}", "{arguments}.0", "{arguments}.1"]
},
start: {
funcName: "gpii.lifecycleManager.start",
args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
// options, solutions, callback
},
update: {
funcName: "gpii.lifecycleManager.update",
args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
} // options, solutions, callback
}
});
// A standard interception point so that the process of resolving names onto
// settings handlers and actions can be mocked for integration tests
fluid.defaults("gpii.lifecycleManager.nameResolver", {
gradeNames: ["fluid.littleComponent", "autoInit"],
invokers: {
resolveName: {
funcName: "fluid.identity"
}
}
});
fluid.defaults("gpii.lifecycleManager.variableResolver", {
gradeNames: ["fluid.eventedComponent", "autoInit"],
components: {
resolverConfig: {
type: "gpii.lifecycleManager.standardResolverConfig"
}
},
members: {
resolvers: {
expander: {
func: "gpii.lifecycleManager.variableResolver.computeResolvers",
args: "{that}.resolverConfig.options.resolvers"
}
},
fetcher: {
expander: {
func: "gpii.resolversToFetcher",
args: "{that}.resolvers"
}
}
},
invokers: {
resolve: {
funcName: "gpii.lifecycleManager.variableResolver.resolve",
args: ["{arguments}.0", "{that}.fetcher", "{arguments}.1"]
}
}
});
gpii.lifecycleManager.variableResolver.computeResolvers = function (resolvers) {
return fluid.transform(resolvers, fluid.getGlobalValue);
};
gpii.lifecycleManager.variableResolver.resolve = function (material, fetcher, extraFetcher) {
return fluid.expand(material, {
bareContextRefs: false,
// TODO: FLUID-4932 - the framework currently has no wildcard
// support in mergePolicy.
mergePolicy: {
0: {
capabilitiesTransformations: {
"*": {
noexpand: true
}
}
}
},
fetcher: gpii.combineFetchers(fetcher, extraFetcher)
});
};
gpii.resolversToFetcher = function (resolvers) {
return function (parsed) {
var resolver = resolvers[parsed.context];
return !resolver? undefined : (
typeof(resolver) === "function" ?
resolver(parsed.path) : fluid.get(resolver, parsed.path));
};
};
gpii.combineFetchers = function (main, fallback) {
return fallback ? function (parsed) {
var fetched = main(parsed);
return fetched === undefined? fallback(parsed) : fetched;
} : main;
};
fluid.defaults("gpii.lifecycleManager.standardResolverConfig", {
gradeNames: ["fluid.littleComponent", "autoInit", "fluid.applyGradeLinkage"],
resolvers: {
environment: "gpii.lifecycleManager.environmentResolver"
}
});
gpii.lifecycleManager.environmentResolver = function (name) {
return process.env[name];
};
// Transforms the handlerSpec (handler part of the payload) to the model
// required by the settingsHandler
gpii.lifecycleManager.specToSettingsHandler = function (solutionId, handlerSpec) {
var returnObj = {};
returnObj[solutionId] = [{
settings: handlerSpec.settings,
options: handlerSpec.options
}];
return returnObj; // NB array removed here
};
gpii.lifecycleManager.responseToSnapshotRules = {
"*.*.settings.*": {
transform: {
type: "value",
inputPath: "oldValue"
}
}
};
fluid.model.escapedPath = function () {
var path = "";
for (var i = 0; i < arguments.length; ++i) {
path = fluid.pathUtil.composePath(path, arguments[i]);
}
return path;
};
// Transform the response from the handler to a format that we can pass back to it
gpii.lifecycleManager.responseToSnapshot = function (solutionId, handlerResponse) {
var unValued = fluid.model.transform(handlerResponse,
gpii.lifecycleManager.responseToSnapshotRules, {isomorphic: true});
// TODO: Should eventually be able to do this final stage through
// transformation too
return fluid.get(unValued, fluid.model.escapedPath(solutionId, "0"),
fluid.model.escapedGetConfig);
};
// Payload example:
// http://wiki.gpii.net/index.php/Settings_Handler_Payload_Examples
// Transformer output:
// http://wiki.gpii.net/index.php/Transformer_Payload_Examples
gpii.lifecycleManager.invokeSettingsHandlers = function (solutionId, settingsHandlers, nameResolver) {
// array just indexed by number, each one holds one handler for this id
var settingsPackage = fluid.transform(settingsHandlers, function (handlerSpec) {
// first prepare the payload for the settingsHandler in question -
// a more efficient implementation might bulk together payloads
// destined for the same handler
var settingsHandlerPayload = gpii.lifecycleManager.specToSettingsHandler(solutionId, handlerSpec);
// send the payload to the settingsHandler
var resolvedName = nameResolver.resolveName(handlerSpec.type, "settingsHandler");
return {
setSettings: function () {
return fluid.invokeGlobalFunction(resolvedName + ".set", [settingsHandlerPayload]);
},
makeSnapshot: function (handlerResponse) {
// update the settings section of our snapshot to contain the new information
var settingsSnapshot = gpii.lifecycleManager.responseToSnapshot(solutionId, handlerResponse);
var handlerCopy = fluid.copy(handlerSpec);
delete handlerCopy.settings;
return $.extend(true, handlerCopy, settingsSnapshot);
}
};
});
var responsePromise = fluid.promise.sequence(fluid.getMembers(settingsPackage, "setSettings"));
var togo = fluid.promise();
responsePromise.then(function (responses) {
console.log("Got responses ", responses);
var snapshots = fluid.transform(responses, function(handlerResponse, i) {
return settingsPackage[i].makeSnapshot(handlerResponse);
});
console.log("Resolving with ", snapshots);
togo.resolve(snapshots);
}, togo.reject);
return togo;
};
gpii.lifecycleManager.invokeAction = function (action, nameResolver) {
var resolvedName = nameResolver.resolveName(action.type, "action");
var defaults = fluid.defaults(resolvedName);
if (!defaults || !defaults.argumentMap) {
fluid.fail("Error in action definition - " + resolvedName +
" cannot be looked up to a function with a proper argument map: ", action);
}
var args = [];
fluid.each(defaults.argumentMap, function (value, key) {
args[value] = action[key];
});
return fluid.invokeGlobalFunction(resolvedName, args);
};
// Returns the results from any settings action, builds up action returns in argument "actionResults"
gpii.lifecycleManager.executeActions = function (solutionId, settingsHandlers, actions, sessionState, nameResolver) {
var settingsReturn;
var sequence = fluid.transform(actions, function (action) {
if (typeof(action) === "string") {
if (action === "setSettings" || action === "restoreSettings") {
return function () {
var expanded = sessionState.localResolver(settingsHandlers);
var settingsPromise = gpii.lifecycleManager.invokeSettingsHandlers(solutionId, expanded, nameResolver);
settingsPromise.then(function (snapshot) {
settingsReturn = snapshot;
});
return settingsPromise;
}
} else {
fluid.fail("Unrecognised string action: " + action);
}
} else {
return function () {
var expanded = sessionState.localResolver(action);
var result = gpii.lifecycleManager.invokeAction(expanded, nameResolver);
if (action.name) {
sessionState.actionResults[action.name] = result;
}
}
}
});
var resolved = fluid.promise.sequence(sequence);
var togo = fluid.promise();
resolved.then(function () {
togo.resolve(settingsReturn);
}, togo.reject);
return togo;
};
// Will return one of the token keys for an active session
// TODO: We need to implement logic to ensure at most one of these is set, or
// to manage logic for superposition of sessions if we permit several (see GPII-102)
gpii.lifecycleManager.getActiveSessionTokens = function (activeSessions) {
return fluid.keys(activeSessions);
};
gpii.lifecycleManager.getSession = function (activeSessions, tokens) {
if (tokens.length === 0) {
fluid.fail("Attempt to get sessions without keys");
} else {
return activeSessions[tokens[0]];
}
};
/**
* Structure of lifecycleManager options:
* userid: userid,
* actions: either start or stop configuration from solutions registry
* settingsHandlers: transformed settings handler blocks
*/
gpii.lifecycleManager.stop = function (that, options, callback) {
var userToken = options.userToken;
var sessionState = that.activeSessions[userToken];
if (!sessionState) {
callback(false);
return;
}
var promises = fluid.transform(sessionState.solutions, function (solution) {
return gpii.lifecycleManager.executeActions(solution.id,
solution.settingsHandlers, solution.lifecycleManager.stop, sessionState, that.nameResolver);
});
// TODO: In theory we could stop all solutions in parallel
var sequence = fluid.promise.sequence(fluid.values(promises));
sequence.then(function () {
delete that.activeSessions[userToken];
callback(true);
});
};
/**
* Update user preferences.
*/
gpii.lifecycleManager.update = function (that, options, solutions, callback) {
var userToken = options.userToken;
var sessionState = that.activeSessions[userToken];
if (!sessionState) {
fluid.fail("User with token ", userToken, " has no active session");
}
var togo = {};
var promises = fluid.transform(solutions, function (solution) {
// This check is redundant. Currently PCP is only showing adjusters with dynamic solutions.
// Also, dynamic solutions aren't explicitly distinguished from non-dynamic right now.
// TODO: Add 'dynamic: true' to all dynamic solutions after PCP's visualization of adjusters becomes controlled by MM.
/*if (!solution.dynamic) {
togo.restart = true;
return;
}*/
var solIndex = fluid.find(sessionState.solutions,
function findIndex(sol, index) {
if (sol.id === solution.id) {
return index;
}
}
);
var sol, actions = ["setSettings"];
if (solIndex === undefined) { // TODO: This branch consists of mostly dead code - only tested by isolated web tests for LifecycleManager
sol = fluid.copy(solution);
actions.push(solution.lifecycleManager.start);
} else {
// we remove the solution here since it will be readded by "applySolution"
sol = sessionState.solutions.splice(solIndex, 1)[0];
}
return gpii.lifecycleManager.applySolution(sol, solution, actions, sessionState, that.nameResolver);
});
var sequence = fluid.promise.sequence(promises);
sequence.then(function () {
if (!togo.restart) {
togo.success = true;
}
callback(togo);
});
return sequence;
};
gpii.lifecycleManager.applySolution = function (solution, solutionRecord, actions, sessionState, nameResolver) {
var promise = gpii.lifecycleManager.executeActions(
solutionRecord.id, solutionRecord.settingsHandlers, actions, sessionState, nameResolver);
promise.then(function (snapshot) {
console.log("ApplySolution got snapshot ", snapshot);
solution.settingsHandlers = snapshot;
console.log("Solution is now ", solution);
// TODO: clumsy way of ensuring that the START action is performed just once, and not on further updates
if (solution.lifecycleManager.start) {
delete solution.lifecycleManager.start;
}
sessionState.solutions.push(solution);
});
return promise;
};
gpii.lifecycleManager.start = function (that, options, solutions, callback) {
var userToken = options.userToken;
if (that.activeSessions[userToken]) {
// TODO: develop async architecture to prevent rat's nest of callbacks
that.stop({userToken: userToken}, fluid.identity);
}
var sessionState = $.extend(true, {
actionResults: {}
}, options);
// let the user's token as well as any named action results accumulated
// to date be resolvable for any future action
sessionState.localFetcher = gpii.combineFetchers(
gpii.resolversToFetcher({userToken: userToken}),
gpii.resolversToFetcher(sessionState.actionResults));
sessionState.localResolver = function (material) {
return that.variableResolver.resolve(material, sessionState.localFetcher);
};
// Data is an array of solutions with settingsHandlers and
// launchHandlers for each solution
sessionState.solutions = [];
var promises = fluid.transform(solutions, function (solution) {
// build structure for returned values (for later reset)
var togo = fluid.copy(solution);
return gpii.lifecycleManager.applySolution(togo, solution, solution.lifecycleManager.start, sessionState, that.nameResolver);
});
that.activeSessions[userToken] = sessionState;
var sequence = fluid.promise.sequence(promises);
sequence.then(function () {
callback(true);
});
};
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment