Ghost/ghost/adapter-manager/lib/AdapterManager.js
Naz 37dd187fe6
Added adapter caching based on features
refs https://github.com/TryGhost/Toolbox/issues/384

- Adapter cache was not able to store multiple object instances derived from same Base class. This created a need to create boilerplate "shell" classes inheriting from the Base class, e.g.: ImageSizeCacheSyncInMemory etc.
- Having feature-based adapter instance caching in the adapter manager allows to simplify configuration and reuse the "base class" instead of creating artificial "shell" classes.
- For example with this change both image sizes and settings caches will create separate cache instances deriving from default "Memory" class. Less code, less configuration!
2022-09-06 17:51:57 +08:00

153 lines
5.1 KiB
JavaScript

const path = require('path');
const errors = require('@tryghost/errors');
/**
* @typedef { function(new: Adapter, object) } AdapterConstructor
*/
/**
* @typedef {object} Adapter
* @prop {string[]} requiredFns
*/
module.exports = class AdapterManager {
/**
* @param {object} config
* @param {string[]} config.pathsToAdapters The paths to check, e.g. ['content/adapters', 'core/server/adapters']
* @param {(path: string) => AdapterConstructor} config.loadAdapterFromPath A function to load adapters, e.g. global.require
*/
constructor({pathsToAdapters, loadAdapterFromPath}) {
/**
* @private
* @type {Object.<string, AdapterConstructor>}
*/
this.baseClasses = {};
/**
* @private
* @type {Object.<string, Object.<string, Adapter>>}
*/
this.instanceCache = {};
/**
* @private
* @type {string[]}
*/
this.pathsToAdapters = pathsToAdapters;
/**
* @private
* @type {(path: string) => AdapterConstructor}
*/
this.loadAdapterFromPath = loadAdapterFromPath;
}
/**
* Register an adapter type and the corresponding base class. Must be called before requesting adapters of that type
*
* @param {string} type The name for the type of adapter
* @param {AdapterConstructor} BaseClass The class from which all adapters of this type must extend
*/
registerAdapter(type, BaseClass) {
this.instanceCache[type] = {};
this.baseClasses[type] = BaseClass;
}
/**
* getAdapter
*
* @param {string} adapterName The name of the type of adapter, e.g. "storage" or "scheduling", optionally including the feature, e.g. "storage:files"
* @param {string} adapterClassName The active adapter instance class name e.g. "LocalFileStorage"
* @param {object} [config] The config the adapter could be instantiated with
*
* @returns {Adapter} The resolved and instantiated adapter
*/
getAdapter(adapterName, adapterClassName, config) {
if (!adapterName || !adapterClassName) {
throw new errors.IncorrectUsageError({
message: 'getAdapter must be called with a adapterName and a adapterClassName.'
});
}
let adapterType;
if (adapterName.includes(':')) {
[adapterType] = adapterName.split(':');
} else {
adapterType = adapterName;
}
const adapterCache = this.instanceCache[adapterType];
if (!adapterCache) {
throw new errors.NotFoundError({
message: `Unknown adapter type ${adapterType}. Please register adapter.`
});
}
// @NOTE: example cache key value 'email:newsletters:custom-newsletter-adapter'
const adapterCacheKey = `${adapterName}:${adapterClassName}`;
if (adapterCache[adapterCacheKey]) {
return adapterCache[adapterCacheKey];
}
/** @type AdapterConstructor */
let Adapter;
for (const pathToAdapters of this.pathsToAdapters) {
const pathToAdapter = path.join(pathToAdapters, adapterType, adapterClassName);
try {
Adapter = this.loadAdapterFromPath(pathToAdapter);
if (Adapter) {
break;
}
} catch (err) {
// Catch runtime errors
if (err.code !== 'MODULE_NOT_FOUND') {
throw new errors.IncorrectUsageError({err});
}
// Catch missing dependencies BUT NOT missing adapter
if (!err.message.includes(pathToAdapter)) {
throw new errors.IncorrectUsageError({
message: `You are missing dependencies in your adapter ${pathToAdapter}`,
err
});
}
}
}
if (!Adapter) {
throw new errors.IncorrectUsageError({
message: `Unable to find ${adapterType} adapter ${adapterClassName} in ${this.pathsToAdapters}.`
});
}
const adapter = new Adapter(config);
if (!(adapter instanceof this.baseClasses[adapterType])) {
if (Object.getPrototypeOf(Adapter).name !== this.baseClasses[adapterType].name) {
throw new errors.IncorrectUsageError({
message: `${adapterType} adapter ${adapterClassName} does not inherit from the base class.`
});
}
}
if (!adapter.requiredFns) {
throw new errors.IncorrectUsageError({
message: `${adapterType} adapter ${adapterClassName} does not have the requiredFns.`
});
}
for (const requiredFn of adapter.requiredFns) {
if (typeof adapter[requiredFn] !== 'function') {
throw new errors.IncorrectUsageError({
message: `${adapterType} adapter ${adapterClassName} is missing the ${requiredFn} method.`
});
}
}
adapterCache[adapterCacheKey] = adapter;
return adapter;
}
};