Created
October 20, 2014 21:46
-
-
Save amb26/cc967b0715fbf6a5f54e to your computer and use it in GitHub Desktop.
Asynchrouwnous LifecycleManager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
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