You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cvm/src/packages/dlayer-old/load-modules.js

129 lines
4.1 KiB
JavaScript

"use strict";
// const mergeByTemplate = require("merge-by-template");
const deepMerge = require("./deep-merge");
const syncpipe = require("syncpipe");
/*
Take a list of modules; each module specifies a name, schema root, types, type extensions, and a context factory function. Each module is internally assigned a unique ID. This unique ID is associated with each type factory and type extension method, and used as the key for a map of context factories; that way, upon invoking those methods, the module's own corresponding context can be injected. Only a single context should be created per module per request, so there should be a cache layer for the contexts (keyed by module ID), with each request creating a new cache.
*/
// NOTE: This can be global because we identify existing assignments by object identity, and that will never conflict
let numberedModules = new WeakMap();
let currentModuleNumber = 0;
function getModuleID(module) {
if (!numberedModules.has(module)) {
numberedModules.set(module, currentModuleNumber++);
}
return numberedModules.get(module);
}
function createTypeTracker() {
let typeFactories = {};
return {
add: function (module, name, factory) {
if (typeFactories[name] != null) {
let existingEntry = typeFactories[name];
throw new Error(`Type '${name}' already exists (from module '${module.name}', already defined by module '${existingEntry.source.name}')`);
} else {
typeFactories[name] = {
source: module,
// No context provided to type factory functions for now, since they are not allowed to be async for now anyway
// FIXME: Maybe add a warning if the user returns a Promise from a factory, asking them to file a bug if they really need it?
// func: wrapModuleFunction(module, factory)
func: factory
};
}
},
get: function () {
return typeFactories;
}
};
}
function createExtensionTracker() {
let extendedTypes = {};
return {
add: function (module, type, name, method) {
if (extendedTypes[type] == null) {
extendedTypes[type] = {};
}
let extensions = extendedTypes[type];
if (extensions[name] != null) {
let existingEntry = extensions[name];
throw new Error(`Type '${type}' already has a method extension named '${name}' (from module '${module.name}', already defined by module '${existingEntry.source.name}')`);
} else {
extensions[name] = {
source: module,
func: wrapModuleFunction(module, method)
};
}
},
get: function () {
return extendedTypes;
}
};
}
function wrapModuleFunction(module, func) {
return { __moduleID: getModuleID(module), func: func };
}
function defaultContext() {
// Fallback function that generates an empty context, for when a module doesn't specify a makeContext handler
return {};
}
module.exports = function (modules) {
// TODO: Eventually replace hand-crafted merging logic with merge-by-template, once it can support this usecase properly(tm)
// TODO: Fix merge-by-template so that reasonable error messages can be generated here, that are actually aware of eg. the conflicting key
let types = createTypeTracker();
let typeExtensions = createExtensionTracker();
let contextFactories = syncpipe(modules, [
_ => _.map((module) => [ getModuleID(module), module.makeContext ?? defaultContext ]),
_ => new Map(_)
]);
let schemaRoots = modules.map((module) => module.root ?? {});
for (let module of modules) {
for (let [ type, factory ] of Object.entries(module.types ?? {})) {
types.add(module, type, factory);
}
for (let [ type, extensions ] of Object.entries(module.extensions ?? {})) {
for (let [ name, method ] of Object.entries(extensions)) {
typeExtensions.add(module, type, name, method);
}
}
}
return {
root: schemaRoots.reduce(deepMerge, {}),
types: types.get(),
extensions: typeExtensions.get(),
makeContextFactory: function (baseContext) {
let cache = new Map();
return function makeContextForModule(moduleID) {
if (!cache.has(moduleID)) {
cache.set(moduleID, {
... baseContext,
... contextFactories.get(moduleID)()
});
}
return cache.get(moduleID);
};
}
};
};