singletons
Version:
Helps create and manage families of singletons based on customizable conditions
199 lines (160 loc) • 6.35 kB
JavaScript
import keyFunc from 'keyfunc';
const getKeyFunc = function (defaultKeyfunc) {
let keyfunc = defaultKeyfunc;
if (typeof keyfunc !== 'function') {
if (Array.isArray(keyfunc)) {
keyfunc = keyFunc(...keyfunc);
} else {
throw new TypeError(
`Initializing keyFunc argument should be a function generating unique
keys from arguments, or an array of hints`);
}
}
return keyfunc;
};
const idFunc = args => args;
export const SingletonFactory = function (
Type, defaultKeyfunc = obj => obj.toString(), options = {}) {
const keyfunc = getKeyFunc(defaultKeyfunc);
// Wholesale preprocessing (before everything except direct type conversion)
const preprocess = options.preprocess || idFunc;
// Wholesale postprocessing (last chance to update the current singleton)
const postprocess = options.postprocess || idFunc;
// If defined then this Type itself is spreadable (the singleton is some
// kind of container)
const spread = options.spread;
const shallowSpread = options.shallowSpread;
// If not undefined, some args will require special processing
const customArgs = options.customArgs && new Map(options.customArgs) ||
spread || shallowSpread && new Map([Type, {spread, shallowSpread}]);
if (options.customArgs && (spread || shallowSpread)) {
// If both are true, then customArgs already exists but is still missing
// this Type as special arg (or not! But then it's a user mistake that
// yields here a silent override)
customArgs.set(Type, {spread, shallowSpread});
}
// When special handling is activated, reduceableTypes will help merge
// several args into one
const reduceableTypes = customArgs ? new Set(Array.from(customArgs.keys())
.filter(key => {
return customArgs.get(key).reduce;
})) : null;
// When special handling is activated, spreadableTypes will help split
// one arg into several
const spreadableTypes = customArgs ?
new Set(Array.from(customArgs.keys())
.filter(key => {
const customArg = customArgs.get(key);
return customArg.spread || customArg.shallowSpread;
})) : null;
// Helper function to split registered containerlike args into the args
// they contain
const spreadArgs = (array, arg) => {
let newArray;
const type = Object.getPrototypeOf(arg).constructor;
if (spreadableTypes.has(type)) {
const {spread, shallowSpread} = customArgs.get(type);
newArray = spread === true ?
Array.from(arg).reduce(spreadArgs, []) :
spread !== undefined ?
spread(arg).reduce(spreadArgs, []) :
shallowSpread === true ?
Array.from(arg) :
shallowSpread(arg);
} else {
newArray = [arg];
}
return array.concat(newArray);
};
// Helper function to encapsulate all preprocessing
const preprocessAll = _args => {
let extractedArgs;
let convertedArgs;
let unreduceableArgs;
if (customArgs) {
// First distinguish between regular init args and special treatment args
extractedArgs = _args.filter(arg =>
customArgs.has(Object.getPrototypeOf(arg).constructor));
// Pass through regular args as already converted;
// Spread spreadable special args;
// Convert convertible special args;
// Filter out all other known special args as they are meaningless
// to instanciate Type
convertedArgs = _args.reduce(spreadArgs, []).map(arg => {
const type = Object.getPrototypeOf(arg).constructor;
if (customArgs.has(type)) {
const {convert, spread, shallowSpread} = customArgs.get(type);
return convert ? convert(arg) : spread || shallowSpread ? arg : null;
}
return arg;
}).filter(arg => arg !== null);
// Filter special args that won't need postprocess reduction
unreduceableArgs = extractedArgs.filter(arg => {
const type = Object.getPrototypeOf(arg).constructor;
return !reduceableTypes.has(type);
});
}
// convertedArgs contains also regular args;
// After this, all args have the correct types and order to instanciate or
// recall a unique instance of type Type
const args = preprocess(convertedArgs || _args);
return {args, extractedArgs, unreduceableArgs};
};
// All singletons from this Singleton type ever created
const instances = new Map();
// The unique field marking every instance of type Type created by the
// following Singleton
const keySymb = Symbol();
// The custom Singleton factory out of this Singleton factory
const Singleton = function (..._args) {
const {args, extractedArgs, unreduceableArgs} = preprocessAll(_args);
const key = Singleton.key(...args);
let instance = instances.get(key);
// If the singleton doesn't exist, create it, mark it, register it
if (!instance) {
instance = new Type(...args);
instance[keySymb] = key;
instances.set(key, instance);
}
// If special args, now use them to update the singleton instance
if (customArgs) {
// Reduce reduceable special args and postprocess them
reduceableTypes.forEach(type => {
const {reduce, postprocess} = customArgs.get(type);
postprocess.call(instance, reduce(extractedArgs.filter(
arg => arg instanceof type)));
});
// Postprocess unreduceable special args
unreduceableArgs.forEach(arg => {
const type = Object.getPrototypeOf(arg).constructor;
const {postprocess} = customArgs.get(type) || {};
if (postprocess) {
postprocess.call(instance, arg);
}
});
}
// Last call for preprocessing
postprocess.call(instance, args);
return instance;
};
Singleton.key = (arg0, ...args) => {
if (arg0[keySymb] && !args.length) {
return arg0[keySymb];
}
return keyfunc(arg0, ...args);
};
Singleton.singleton = function (_key) {
return instances.get(_key);
};
Singleton.get = function (...args) {
return instances.get(Singleton.key(...args));
};
Singleton.looseKey = (..._args) => {
const {args} = preprocessAll(_args);
return Singleton.key(...args);
};
Singleton.looseGet = function (...args) {
return instances.get(Singleton.looseKey(...args));
};
return Singleton;
};