Source: instance-updater.ts

/**
 * @author Michael Hasenstein <hasenstein@yahoo.com>
 * @copyright REFINIO GmbH 2017
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

/**
 * @module
 */

/**
 * Options object for instance-updater, which updates the currently active instance.
 * @global
 * @typedef {object} InstanceUpdateOptions
 * @property {Recipe[]} [recipes=[]]
 * @property {Module[]} [modules=[]]
 */
export interface InstanceUpdaterOptions {
    recipes?: InstanceOptions['initialRecipes'];
    modules?: readonly Module[];
}

import {addRecipesToRuntimeAndStorage} from './crdt-recipes';
import {createError} from './errors';
import type {InstanceOptions} from './instance';
import {getInstanceIdHash} from './instance';
import type {Instance, Module} from './recipes';
import type {VersionedObjectResult} from './storage-versioned-objects';
import {getObjectByIdHash, storeVersionedObject} from './storage-versioned-objects';

/**
 * Use this module through {@link instance.module:ts.registerRecipes|instance.registerRecipes} for
 * your convenience. It provides the `name` and `owner` from the active instance
 * and registers any given {@link Recipe|Recipes} with the currently running instance, which
 * this function does not do since you could create or update an inactive instance.
 * Note that recipes are *not* added to the runtime, since you might be updating an inactive
 * Instance object.
 * @static
 * @async
 * @param {InstanceUpdaterOptions} options
 * @returns {Promise<ObjectCreation[]>} Returns the result of creating the Instance object and,
 * if provided Recipe and Module objects. The Instance object creation result always is in the
 * first position and always exists. The {@link Person} object creation result is in second
 * place, but it only exists if the Person object had to be created. {@link Recipe} and
 * {@link Module} creation results only exist if any recipes or modules were provided.
 */
export async function updateInstance({
    recipes = [],
    modules = []
}: InstanceUpdaterOptions): Promise<VersionedObjectResult<Instance>> {
    const instanceIdHash = getInstanceIdHash();

    if (instanceIdHash === undefined) {
        throw createError('INU-CRO');
    }

    const {obj: instanceObj} = await getObjectByIdHash(instanceIdHash, 'Instance');

    // Create caches used to remove duplicates, when newly added modules or recipes have already
    // been registered previously
    const existingRecipeObjHashes = new Set(instanceObj.recipe);
    const existingModuleObjHashes = new Set(instanceObj.module);

    const recipeResults = await addRecipesToRuntimeAndStorage(recipes);

    const moduleCreation = await Promise.all(
        modules.map(module => {
            // Unlike recipes (just above) which have a lot of rules that go beyond what is checked
            // by the regular ONE object to/from microdata conversion code modules are much more
            // simple. It is sufficient to test the "type" For the rest the built-in checks
            // using the "Module" recipe, applied when storing the object just below,are enough.
            if (module.$type$ !== 'Module') {
                throw createError('INU-CRO1', {modules});
            }

            return storeVersionedObject(module);
        })
    );

    instanceObj.recipe.push(
        ...recipeResults
            .filter(result => !existingRecipeObjHashes.has(result.hash))
            .map(result => result.hash)
    );

    instanceObj.module.push(
        ...moduleCreation
            .filter(result => !existingModuleObjHashes.has(result.hash))
            .map(result => result.hash)
    );

    return await storeVersionedObject(instanceObj);
}