UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

951 lines (868 loc) 51.1 kB
/* Copyright The Infusion copyright holders See the AUTHORS.md file at the top-level directory of this distribution and at https://github.com/fluid-project/infusion/raw/main/AUTHORS.md. Licensed under the Educational Community License (ECL), Version 2.0 or the New BSD license. You may not use this file except in compliance with one these Licenses. You may obtain a copy of the ECL 2.0 License and BSD License at https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt */ "use strict"; /** Explodes a localised filename, perhaps with extension, into a number of variants with the basename followed * by underscores and increasingly specialised locale names, taking into account a possible default locale. * E.g. if `filename` is `messages.json`, `locale` is `fr_CH` and `defaultLocale` is `en`, this function will return * `["messages_en.json", "messages_fr.json", "messages_fr_CH.json"]`. * * This is similar to the algorithm specified for localised resources in Java, e.g. documented at * https://docs.oracle.com/javase/6/docs/api/java/util/ResourceBundle.html#getBundle%28java.lang.String,%20java.util.Locale,%20java.lang.ClassLoader%29 * @param {String} fileName - The base filename or URL to be exploded * @param {String} [locale] - A locale name with respect to which to perform the explosion * @param {String} [defaultLocale] - An optional default locale to fall back on in the case none of the localised * variants could be located. * @return {String[]} An array of localised filenames to be fetched, in increasing order of specificity. In * practice, we expect the last member of this array which can be fetched to correspond to the most desirable * localised version of the resource. */ fluid.explodeLocalisedName = function (fileName, locale, defaultLocale) { var lastDot = fileName.lastIndexOf("."); if (lastDot === -1 || lastDot === 0) { lastDot = fileName.length; } var baseName = fileName.substring(0, lastDot); var extension = fileName.substring(lastDot); var segs = locale ? locale.split("_") : []; var exploded = fluid.transform(segs, function (seg, index) { var shortSegs = segs.slice(0, index + 1); return baseName + "_" + shortSegs.join("_") + extension; }); if (defaultLocale) { exploded.unshift(baseName + "_" + defaultLocale + extension); } exploded.unshift(fileName); return exploded; }; /** A specification for a (possibly asynchronously available) resource. This might be fetched a URL, * the filesystem, or some other source requiring an asynchronous interaction. These are duck-typed by the * presence of particular fields, such as `url` or `path` which signal to the decoder that a particular * implementation should be activated in a particular environment (e.g. an AJAX request or the node HTTP API) * @typedef {Object} ResourceSpec * @member {String} [locale] An optional locale which will be used to look for a localised variant of a resource * @member {String} [defaultLocale] A fallback locale to be used in the case that the variant localised to `locale` * cannot be loaded * @member {String} [dataType] An optional specification of a data type - this may be "JSON", "HTML" or some other * value indicating that the fetched resource will be parsed into some further representation before it is placed * into the `parsed` field of the resulting resource. */ /** A ResourceSpec designating that a resource will be loaded from a URL via an HTTP or HTTPS request. * @typedef {ResourceSpec} UrlResourceSpec * @member {String} url - The full url from which the resource will be loaded. * @member {Object} options - A freeform options structure which will be forwarded without interpretation to the * underlying transport, e.g. XmlHttpRequest or node's http/https request */ /** A ResourceSpec designating that a resource will be loaded from the filesystem * @typedef {ResourceSpec} PathResourceSpec * @member {String} path - The path in the filesystem from which the resource will be loaded. This will be sent * to `fluid.resolveModulePath` and so may begin with a module-relative specification such as "%infusion" * @member {String} charEncoding - The character encoding to be used when reading the file - this defaults to "utf-8" */ /** A ResourceSpec designating that a resource will be loaded from a DataSource * @typedef {ResourceSpec} DataSourceResourceSpec * @member {String} dataSource - An IL reference to a DataSource whose `get` method will be used to fetch the resource * @member {Object} [directModel] - An optional argument to be sent to the DataSource's `get` method to specify the resource * to be loaded */ /** A free hash of names to resourceSpec structures, the currency of many functions in this file * @typedef {Object.<String, ResourceSpec>} ResourceSpecs */ /** The options provided to construct a ResourceFetcher * @typedef {Object} ResourceFetcherOptions * @member {String} defaultLocale - The value of `defaultLocale` to be imputed for any `ResourceSpec` entry that has * not filled it in */ /** Accepts a hash of free keys to ResourceSpecs and a callback. The fetch of these will be initiated, and the * callback called with the fetched resources when they are all complete. * @param {Object.<String, ResourceSpec>} resourceSpecs - Hash of keys to ResourceSpecs designating resources to be fetched. * This will be modified tby this function * @param {Function} callback - A callback which will receive the filled-in resourceSpecs structure when I/O is complete * @param {ResourceFetcherOptions} options - A structure of options to a `ResourceFetcher`. * @return {ResourceFetcher} A lightweight component (not an Infusion component) coordinating the I/O process */ fluid.fetchResources = function (resourceSpecs, callback, options) { var that = fluid.makeResourceFetcher(resourceSpecs, callback, options, fluid.identity); that.optionsReady.resolve(); that.fetchAll(); return that; }; /** The concept behind the "explode/condense" for Locales group of functions is to implement a straightforward * though unperformant model of client-side fallback localisation. Each user-supplied resourceSpec is exploded * into a series of progressively less refined locale fallback variants. Each of these is then fetched, * and then the results are recombined after fetching in order to only report a resource to the root user's * spec for the most specific localised variant that made a response. */ /** Explode the modifiable `resourceSpec` structures in ths supplied resourceFetcher into an expanded set holding * one entry for each localised variant that should be queried when using a fallback algorithm. For each * `resourceSpec` this will be generated by calling `fluid.explodeLocalisedName` on the resourceSpec's path/url * field, storing the results of this in a new `resourceSpec` field `localeExploded` and generating an array of * fresh resourceSpecs in a field `localeExplodedPaths`. * @param {resourceFetcher} resourceFetcher - The parent `resourceFetcher` holding the resourceSpecs. This houses * the writeable `resourceSpecs` structure, for which each element will be modified by this function, * to include new fields `localeExploded`, `localeExplodedSpecs` and in addition, the `locale` and `defaultLocale` fields * be filled in. */ fluid.fetchResources.explodeForLocales = function (resourceFetcher) { fluid.each(resourceFetcher.resourceSpecs, function (resourceSpec) { // If options.defaultLocale is set, it will be a default for defaultLocale set on an individual resourceSpec resourceSpec.resolvedDefaultLocale = resourceSpec.defaultLocale || resourceFetcher.options.defaultLocale; // A locale from the model takes priority over any locally set locale - should review this resourceSpec.resolvedLocale = resourceFetcher.options.locale || resourceSpec.locale || resourceSpec.resolvedDefaultLocale; // Note: An important change from Infusion 4.3 - EVERY resourceSpec with a path now gets upgraded to localeExplodedSpecs, // since we could never be sure that they wouldn't get localised as a result of a change to model.resourceLoader.locale // and the "loader" gets established as part of the very first pass in resolveLoaderTask // Fix for FLUID-6750 if (!resourceSpec.loader.loader.noPath) { var pathKey = resourceSpec.loader.pathKey; var path = resourceSpec[pathKey]; var resolvedPath = resourceSpec[pathKey] = resourceFetcher.transformResourceURL(path); resourceSpec.localeExploded = fluid.explodeLocalisedName(resolvedPath, resourceSpec.resolvedLocale, resourceSpec.resolvedDefaultLocale); resourceSpec.localeExplodedSpecs = fluid.transform(resourceSpec.localeExploded, function (oneExploded) { var togo = { loader: resourceSpec.loader, options: resourceSpec.options }; togo[pathKey] = oneExploded; return togo; }, fluid.fetchResources.prepareRequestOptions); } }); }; /** Accepts an array of ResourceSpecs as exploded by `fluid.fetchResources.explodeForLocales` into a * member `localeExplodedSpecs" and sets up I/O to query them for the matching resource. The current implementation * will query each exploded spec regardless of error, and the results will be collated by `fluid.fetchResources.condenseExplodedLocales" * @param {ResourceSpec[]} localeExplodedSpecs - Array of ResourceSpecs to be queried for the appropriately localised * version of a resource * @param {OneResourceLoader} loader - a loader suitable for loading all specs in the array * @return {Promise} A promise which resolves to an array of structures each holding `{resolved: payload}` or * `{rejected: error}` for each queried resource */ fluid.fetchResources.launchExplodedLocales = function (localeExplodedSpecs, loader) { var promiseArray = fluid.transform(localeExplodedSpecs, loader, function (promise) { var promiseToGo = fluid.promise(); promise.then(function (resolve) { promiseToGo.resolve({resolved: resolve}); }, function (error) { promiseToGo.resolve({rejected: error}); }); return promiseToGo; }); var settledArrayPromise = fluid.promise.sequence(promiseArray); return settledArrayPromise; }; /** Accepts the settled promise array dispensed from `fluid.fetchResources.launchExplodedLocales` and the original * resourceSpec, and condenses back into a single promise picking either the first successfully resolved request, * if any, or a rolled-up error payload * @param {ResourceSpec} resourceSpec - The original resourceSpec that gave rise to the `localeExplodedSpecs` that * gave rise to the supplied `settledArray` * @param {Object[]} settledArray - The array of resolutions yielded by the promise returned from * `fluid.fetchResources.launchExplodedLocales` * @return {Promise} A promise yielding either the first successful fetch for a localised resource, or a rolled-up * error listing the paths which were queried */ fluid.fetchResources.condenseExplodedLocales = function (resourceSpec, settledArray) { var togo = fluid.promise(); settledArray.reverse(); var lastNonError = fluid.find(settledArray, function (settled) { return settled.resolved; }); if (lastNonError) { togo.resolve(lastNonError); } else { togo.reject({ isError: true, message: "No localised variants of the resource could be found at any of the paths " + resourceSpec.localeExploded.join(", ") }); } return togo; }; /** Given a resourceSpec and a suitable loader for its resource, return a task which yields either a localised * version of the reource, if it had been determined to require localisation by `fluid.fetchResources.explodeForLocales`, * or else just the simple action of the supplied loader on the resource * @param {ResourceSpec} resourceSpec - The resourceSpec for which a loader task is to be resolved * @param {OneResourceLoader} loader - A loader suitable for loading the supplied resource * @return {Task} A task which loads the resource */ fluid.fetchResources.resolveLoaderTask = function (resourceSpec, loader) { if (resourceSpec.localeExplodedSpecs) { return function () { var togo = fluid.promise(); var settledArrayPromise = fluid.fetchResources.launchExplodedLocales(resourceSpec.localeExplodedSpecs, loader); settledArrayPromise.then(function (settledArray) { var condensed = fluid.fetchResources.condenseExplodedLocales(resourceSpec, settledArray); fluid.promise.follow(condensed, togo); }); return togo; }; } else { return function () { return loader(resourceSpec); }; } }; /** Invoked at the resolution of each individual I/O process in order to check whether the resource fetching process * as a whole has reached completion - if so, the overall `completionPromise` is fired. * @param {ResourceSpecs} resourceSpecs - The complete set of ResourceSpecs in the process of being fetched. The * promise disposition status of each of these will be checked to see if any of them is still pending. * @param {ResourceFetcher} resourceFetcher - The `resourceFetcher` managing the overall fetch process. Its * `completionPromise` will be resolved if no pending I/O remains. */ fluid.fetchResources.checkCompletion = function (resourceSpecs, resourceFetcher) { var incomplete = fluid.find_if(resourceSpecs, function (resourceSpec) { return !resourceSpec.promise.disposition; }); if (!incomplete) { // Close over this since it might get re-initialised var completionPromise = resourceFetcher.completionPromise; // Always defer notification in an anti-Zalgo scheme to ease problems like FLUID-6202 fluid.invokeLater(function () { if (!completionPromise.disposition) { completionPromise.resolve(resourceSpecs); } }); } }; /** The `options` structure fired to the transforming promise chain of the `resourceSpec`'s `event`. * @typedef {Object} ResourceSpecTransformOptions * @member {ResourceSpec} resourceSpec - The `resourceSpec` for which this chain is executing * @member {ResourceFetcher} resourceFetcher - The overall `resourceFetcher` governing the fetch process for the * complete set of loading resources */ /** An impure member of the `transforming promise chain` for an individual `resourceSpec` that will stash the * resolved value of its predecessor (which will be the `OneResourceLoader`) into the `resourceText` member of the * `resourceSpec` * @param {String} resourceText - The resource text loaded by the previous transform chain element * @param {ResourceSpecTransformOptions} options - The transform chain's options structure * @return {String} resourceText - The unchanged value of the supplied resource text */ fluid.fetchResources.noteResourceText = function (resourceText, options) { options.resourceSpec.resourceText = resourceText; return resourceText; }; /** An impure member of the `transforming promise chain` for an individual `resourceSpec` that will stash the * resolved value of its predecessor (which will be the `ResourceParser`) into the `parser` member of the * `resourceSpec` * @param {Any} parsed - The parsed representation produced by the `ResourceParser` * @param {ResourceSpecTransformOptions} options - The transform chain's options structure * @return {Any} - The unchanged value of the parsed resource value */ fluid.fetchResources.noteParsed = function (parsed, options) { options.resourceSpec.parsed = parsed; return parsed; }; fluid.fetchResources.fireFetched = function (parsed, options) { options.resourceSpec.onFetched.fire(parsed); return parsed; }; /** Prepare the `options` member of a `resourceSpec` by copying in the top-level element with the matching pathKey * TODO: Determine why on earth we still do this */ fluid.fetchResources.prepareRequestOptions = function (resourceSpec) { var pathKey = resourceSpec.loader.pathKey; var requestOptions = {}; requestOptions[pathKey] = resourceSpec[pathKey]; resourceSpec.options = $.extend(true, {}, resourceSpec.options, requestOptions); return resourceSpec; }; fluid.fetchResources.initOneResource = function (resourceSpec) { resourceSpec.promise = fluid.promise(); resourceSpec.launched = false; }; // TODO: We will have to split this up into two to allow resourceSpecs to be "rearmed" after construction. // The chain is reusable but the resulting promise is not. /** Subscribe one `resourceSpec` to the fetch process by constructing its pseudoevent `event` governing the * transforming promise chain, looking up its loader and parser, and adding them as listeners in this chain * together with other standard elements. * @param {ResourceSpec} resourceSpec - The `resourceSpec` to be subscribed * @param {String} key - The key by which the `resourceSpec` is index in its `resourceSpecs` structure * @param {String} ownerComponentId - The id of any component holding the overall ResourceFetcher structure (for debuggability) */ fluid.fetchResources.subscribeOneResource = function (resourceSpec, key, ownerComponentId) { if (resourceSpec.transformEvent) { fluid.fail("Cannot subscribe resource ", resourceSpec, " which has already been subscribed for I/O"); } resourceSpec.key = key; // To aid debuggability resourceSpec.transformEvent = fluid.makeEventFirer({ name: "Transform chain for resource \"" + key + "\"", ownerId: ownerComponentId }); resourceSpec.transformEvent.addListener(fluid.fetchResources.noteParsed, "noteParsed", "last"); var parser = fluid.resourceLoader.resolveResourceParser(resourceSpec); resourceSpec.transformEvent.addListener(parser, "parser", "before:noteParsed"); resourceSpec.transformEvent.addListener(function (parsed) { return fluid.resourceLoader.renderImmutable(parsed, resourceSpec); }, "renderImmutable", "after:parser"); resourceSpec.transformEvent.addListener(fluid.fetchResources.noteResourceText, "resourceText", "before:parser"); resourceSpec.transformEvent.addListener(fluid.fetchResources.resolveLoaderTask(resourceSpec, resourceSpec.loader.loader), "loader", "before:resourceText"); resourceSpec.onFetched = fluid.makeEventFirer({ name: "onFetched event for resources \"" + key + "\"", ownerId: ownerComponentId }); resourceSpec.onError = fluid.makeEventFirer({ name: "onError event for resources \"" + key + "\"", ownerId: ownerComponentId }); resourceSpec.transformEvent.addListener(fluid.fetchResources.fireFetched, "fireFetched", "last"); fluid.fetchResources.prepareRequestOptions(resourceSpec); fluid.fetchResources.initOneResource(resourceSpec); }; /** @name fluid.fetchResources.FetchOne * @class * @member {Promise} promise - A promise for the resolved value nested within the resource indirected by the path segments * @member {ResourceSpec} resourceSpec - The resourceSpec designating the resource */ /** A utility class (in the prototypal sense) to aid resolution of trailing path segments into an asynchronously * fetched resource. It returns a self-trundling series of instances that consume successive path segments until * the final value is dereferenced. From this instance, the members `promise` * @param {ResourceSpec} resourceSpec - The resourceSpec holding the resource that will be dereferenced into * @param {ResourceFetcher} resourceFetcher - The overall resourceFetcher governing the owning component's resources * @param {String[]} segs - The array of path segments to be resolved into the resource once it loads */ fluid.fetchResources.FetchOne = function (resourceSpec, resourceFetcher, segs) { var FetchOne = this; FetchOne.resourceFetcher = resourceFetcher; FetchOne.resourceSpec = resourceSpec; FetchOne.segs = segs || []; // We don't add a rejection handler here since is a "leaf promise" which will give rise to an unhandled rejection warning. // The overall rejection of resources will surface at the component level. var thisPromise = FetchOne.promise = fluid.promise(); fluid.fetchResources.fetchOneResource(resourceSpec, resourceFetcher).then(function () { thisPromise.resolve(fluid.fetchResources.resolveFetchOne(FetchOne)); }); }; /** Resolve the referenced resource value inside a FetchOne holder. Note that this value will only be * resolvable once the parent resource has loaded. * @param {fluid.fetchResources.FetchOne} FetchOne - The FetchOne holder referencing an asynchronously available resource * @return {Any} The indirected resource value */ fluid.fetchResources.resolveFetchOne = function (FetchOne) { return fluid.getImmediate(FetchOne.resourceSpec, FetchOne.segs); }; /** Invoked by the framework when indirection into the unfetched resource is required * @param {String} seg - The path segment to be indirected into the resource * @return {fluid.fetchResources.FetchOne} A further FetchOne instance indirected further by the supplied path segment */ fluid.fetchResources.FetchOne.prototype.resolvePathSegment = function (seg) { return new fluid.fetchResources.FetchOne(this.resourceSpec, this.resourceFetcher, this.segs.concat(fluid.makeArray(seg))); }; // Note: This strange style of applying JSDoc comments is described at https://stackoverflow.com/questions/23095975/jsdoc-object-methods-with-method-or-property /** The lightweight `resourceFetcher` component (not an Infusion component or a class) coordinating the fetch process * designated by a `resourceSpecs` structure. * @name ResourceFetcher * @class */ /** Perform non-static initialisation of the supplied resourceFetcher - this will be repeated whenever * there is a call to `refetchAll`. Non-static members such as promises will be cancelled and reinitialised * @param {ResourceFetcher} resourceFetcher - The resourceFetcher to be initialised */ fluid.initResourceFetcher = function (resourceFetcher) { /** * @name ResourceFetcher.completionPromise * @member {Promise} The `completionPromise` for the fetcher which will yield the full state of fetched `resourceSpecs` * in either success or failure */ resourceFetcher.completionPromise = fluid.promise(); fluid.fetchResources.explodeForLocales(resourceFetcher); resourceFetcher.onInit.fire(resourceFetcher.completionPromise); }; // A function to tag the type of a Fluid resourceFetcher (primarily to mark it uncopyable) fluid.resourceFetcher = function () {}; /** Construct a lightweight `resourceFetcher` component (not an Infusion component) coordinating the fetch process * designated by a `resourceSpecs` structure. * @param {resourceSpecs} sourceResourceSpecs - The resourceSpecs to be loaded. This will be copied into a modifiable * structure held at `resourceFetcher.resourceSpecs` * @param {Function} callback - An old-fashioned callback to be notified of the condition of the complete status * of the supplied `resourceSpecs` in either success or failure * @param {ResourceFetcherOptions} options - Options governing the entire fetch process (Can include * `locale, `defaultLocale`, `dataType`) * @param {Function} transformResourceURL - A function {String -> String} which maps URL/path entries in resource * specs, possibly by interpolating term values * @return {fluid.resourceFetcher} The constructed resourceFetcher, ready to have individual resources fetched by * an invocation of `fetchOneResource` or the entire set triggered via `fetchAll` */ fluid.makeResourceFetcher = function (sourceResourceSpecs, callback, options, transformResourceURL) { options = options || {}; var that = Object.create(fluid.resourceFetcher.prototype); fluid.extend(that, { sourceResourceSpecs: sourceResourceSpecs, options: fluid.copy(options), // We need to gate the launching of any requests on this promise, since resourceFetcher options arising from // models will arrive strictly later during construction. optionsReady: fluid.promise(), // This may fire multiple times during lifecycle if there are further calls to fetchAll - argument is completionPromise onInit: fluid.makeEventFirer({ name: "onInit for resourceFetcher", ownerId: options.ownerComponentId }), transformResourceURL: transformResourceURL }); /** * @name ResourceFetcher#fetchAll * @function * @see fluid.fetchResources.fetchAll * @return {Promise} The `completionPromise` for the fetcher which will yield the full state of fetched `resourceSpecs` * in either success or failure */ that.fetchAll = function () { return fluid.fetchResources.fetchAll(that); }; that.refetchAll = function () { return fluid.fetchResources.refetchAll(that); }; /** * @name ResourceFetcher#fetchOneResource * @function * @see fluid.fetchResources.fetchOneResource * @param {String} key - The key within this fetcher's `resourceSpecs` for the resource to be fetched * @return {Promise} A promise for the resolution of the resourceSpec's fetched value */ that.fetchOneResource = function (key) { return fluid.fetchResources.fetchOneResource(that.resourceSpecs[key], that); }; /** * @name ResourceFetcher#refetchOneResource * @function * @see fluid.fetchResources.refetchOneResource * @param {String} key - The key within this fetcher's `resourceSpecs` for the resource to be fetched * @return {Promise} A promise for the resolution of the resourceSpec's fetched value */ that.refetchOneResource = function (key) { return fluid.fetchResources.refetchOneResource(that.resourceSpecs[key], that); }; /** * @name ResourceFetcher.resourceSpecs * @member {ResourceSpecs} The fully elaborated `resourceSpecs` structure that will be queried to fetch resources. * This should be considered as volatile and members such as, e.g., `locale` will be updated if the loader is * relocalised, and non-static members such as promises will be reinitialised */ that.resourceSpecs = fluid.copy(that.sourceResourceSpecs); that.onInit.addListener(function (completionPromise) { completionPromise.then(callback, callback); }); fluid.each(options.onInitListeners, function (args) { that.onInit.addListener.apply(null, args); }); fluid.each(that.resourceSpecs, function (resourceSpec) { resourceSpec.dataType = resourceSpec.dataType || that.options.dataType; resourceSpec.loader = fluid.resourceLoader.resolveResourceLoader(resourceSpec); }); fluid.initResourceFetcher(that); fluid.each(that.resourceSpecs, function (resourceSpec, key) { fluid.fetchResources.subscribeOneResource(resourceSpec, key, that.options.ownerComponentId); }); return that; }; /** Trigger the fetching of all resources managed by this `resourceFetcher`. This is typically triggered by the * `onCreate` event of an owning `ResourceLoader`, or else by a standalone invocation of `fluid.fetchResources`. * It will start the process of fetching all resources which have not already been set in flight by individual * calls to `fetchOneResource`. * @param {resourceFetcher} resourceFetcher - The fetcher for which all resources will be loaded * @return {Promise} The `completionPromise` for the fetcher which will yield the full state of fetched `resourceSpecs` * in either success or failure */ fluid.fetchResources.fetchAll = function (resourceFetcher) { fluid.each(resourceFetcher.resourceSpecs, function (resourceSpec) { fluid.fetchResources.fetchOneResource(resourceSpec, resourceFetcher); }); // Deal with FLUID-6441 in case there are no outstanding resources fluid.fetchResources.checkCompletion(resourceFetcher.resourceSpecs, resourceFetcher); return resourceFetcher.completionPromise; }; // A list of the resourceSpec fields which are considered mutable and modified during the fetch process. fluid.fetchResources.mutableResourceSpecFields = ["promise", "resourceText", "parsed", "locale", "defaultLocale"]; /** Reinitialise a resourceFetcher and restart the process of fetching its resource. This will cancel the member `promise`, * and then delete it together with all the other mutable members listed in `mutableResourceSpecFields`. * If the flat `defeatInitiate` is not supplied this will also launch the overall process of refetching this resource * at the `resourceFetcher` level. If `defeatInitiate` is set to `true` it is assumed that the caller will call * `fluit.fetchResources.initiateRefetch` themselves once they have started the refetch for other resources. * @param {resourceSpec} resourceSpec - The `resourceSpec` designating the resource which will be refetched * @param {resourceFetcher} resourceFetcher - The fetcher holding the resource to be refetched * @param {Boolean} [defeatInitiate] - (optional) Unless this is set to `true`, this call will immediately start the * process of refetching the resource * @return {Promise} A promise for the resolution of the resource to be refetched */ fluid.fetchResources.refetchOneResource = function (resourceSpec, resourceFetcher, defeatInitiate) { resourceSpec.promise.cancel(); fluid.fetchResources.mutableResourceSpecFields.forEach(function (field) { delete resourceSpec[field]; }); fluid.fetchResources.initOneResource(resourceSpec); if (!defeatInitiate) { fluid.fetchResources.initiateRefetch(resourceFetcher); } return resourceSpec.promise; }; /** Initiate the overall process of refetching any resources in the supplied `resourceFetcher` whose refetch has bee * scheduled with a previous call to `fluid.fetchResources.refetchOneResource` * @param {resourceFetcher} resourceFetcher - The fetcher for which all resources will be loaded */ fluid.fetchResources.initiateRefetch = function (resourceFetcher) { resourceFetcher.completionPromise.cancel(); delete resourceFetcher.completionPromise; fluid.initResourceFetcher(resourceFetcher, true); fluid.fetchResources.fetchAll(resourceFetcher); resourceFetcher.fetchAll(); }; /** Trigger the refetching of all resources managed by this `resourceFetcher`. By default, this will only fetch resources * which are localisable (that is, they have resource entries with a path field which can be interpolated for a locale) * @param {resourceFetcher} resourceFetcher - The fetcher for which all resources will be loaded * @return {Promise} The `completionPromise` for the fetcher which will yield the full state of fetched `resourceSpecs` * in either success or failure */ fluid.fetchResources.refetchAll = function (resourceFetcher) { var anyLaunched = false; fluid.each(resourceFetcher.resourceSpecs, function (resourceSpec) { if (resourceSpec.resolvedLocale) { anyLaunched = true; fluid.fetchResources.refetchOneResource(resourceSpec, resourceFetcher, true); } }); if (anyLaunched) { fluid.fetchResources.initiateRefetch(resourceFetcher); } return resourceFetcher.completionPromise; }; fluid.fetchResources.fireTransformEvent = function (resourceSpec, resourceFetcher) { return fluid.promise.fireTransformEvent(resourceSpec.transformEvent, null, { resourceSpec: resourceSpec, resourceFetcher: resourceFetcher }); }; /** Trigger the fetching of a single `resourceSpec` from a `resourceFetcher`. This is invoked, for example, * by the core framework on encountering a reference out from the main component's options demanding a value * dependent on the asynchronously resolved `resource` value. * @param {resourceSpec} resourceSpec - The `resourceSpec` designating the resource which will now be fetched * @param {resourceFetcher} resourceFetcher - The overall `resourceFetcher` governing the fetching of all * resources of which the supplied `resourceSpec` must be a member * @return {Promise} A promise for the resolution of the resourceSpec's fetched value */ fluid.fetchResources.fetchOneResource = function (resourceSpec, resourceFetcher) { if (!resourceSpec.launched) { resourceSpec.launched = true; resourceFetcher.optionsReady.then(function () { var transformPromise = fluid.fetchResources.fireTransformEvent(resourceSpec, resourceFetcher); fluid.promise.follow(transformPromise, resourceSpec.promise); // Add these at the last possible moment so that individual resource disposition can beat them // TODO: Convert all these to "new firers" resourceSpec.promise.then(function () { fluid.fetchResources.checkCompletion(resourceFetcher.resourceSpecs, resourceFetcher); }, function (error) { resourceSpec.fetchError = error; resourceSpec.onError.fire(error); resourceFetcher.completionPromise.reject(error); }); }); } return resourceSpec.promise; }; fluid.registerNamespace("fluid.resourceLoader.loaders"); /** A function accepting a resourceSpec and yielding its fetched value * @callback OneResourceLoader * @param {ResourceSpec} resourceSpec - The resourceSpec to be loaded * @return {Promise|Any} A promise for the fetched value of the resource, or the value itself if it could be * loaded synchronously */ /** A structure holding a resolved loader and also the `pathKey` determined to hold the structure member which * holds its path/url based on the duck typing inspection * @typedef {Object} ResolvedResourceLoader * @member {OneResourceLoader} loader - The loader to be used for fetching the resource * @member {String} pathKey - The key by which the field in the `resourceSpec` denoting the resource's path * can be looked up (in practice this will be "url" or "path") */ /** The resourceLoader's listener to the `resourceLoader` area holding live updatable options (primarily locale) * governing refetching of resources. * @param {ResourceFetcher} resourceFetcher - The resourceLoader's `resourceFetcher` member * @param {ResourceFetcherOptions} modelOptions - The updated value of the modelised resourceFetcher options. These will be * re-overlayed on top of the statically configured options * @param {Boolean} early - `true` if this update results from the early initialisation phase of the `resourceLoader`'s model. * Note that this uses the special `earlyModelResolved` event of the ChangeApplier since we require to contribute back * into the model after resources are loaded, prior to the official model initialisation notification of modelListeners * during later construction. */ fluid.resourceLoader.modelUpdated = function (resourceFetcher, modelOptions, early) { resourceFetcher.options = $.extend(true, {}, resourceFetcher.options, modelOptions); if (early) { fluid.fetchResources.explodeForLocales(resourceFetcher); resourceFetcher.optionsReady.resolve(); } else { resourceFetcher.refetchAll(); } }; /** Render a resourceSpec into a form where it may be easily read in the console, primarily by censoring any * component instances such as DataSources that have been expanded into its definition * @param {ResourceSpec} resourceSpec - A resourceSpec to be rendered. * @return {ResourceSpec} A sanitised shallow clone of the resourceSpec */ fluid.resourceLoader.sanitizeResourceSpec = function (resourceSpec) { return fluid.transform(resourceSpec, function (value) { return fluid.isPrimitive(value) || fluid.isPlainObject(value) && !fluid.isPromise(value) ? value : fluid.NO_VALUE; }); }; // "Immutable model resources" for FLUID-6581 fluid.ImmutableArray = function () {}; fluid.ImmutableArray.prototype = []; fluid.ImmutableObject = function () {}; fluid.copyImmutableResource = function (tocopy) { var newContainer = fluid.isArrayable(tocopy) ? new fluid.ImmutableArray() : new fluid.ImmutableObject(); fluid.each(tocopy, function (value, key) { newContainer[key] = value; }); return newContainer; }; // TODO: Break out the impl into a reusable function fluid.resourceLoader.renderImmutable = function (parsed, resourceSpec) { if (resourceSpec.immutableModelResource) { return fluid.copyImmutableResource(parsed); } else { return parsed; } }; /** Given a resourceSpec, look up an appropriate `OneResourceLoader` function for fetching its value based on * inspecting the contents of `fluid.resourceLoader.loaders` for a matching processor for the duck typing field. * If no loader can be located, an exception will be thrown * @param {ResourceSpec} resourceSpec - The resourceSpec for which the loader is to be looked up * @return {ResolvedResourceLoader} A structure holding both the loader and also the key for the corresponding * duck typing field */ fluid.resourceLoader.resolveResourceLoader = function (resourceSpec) { var loaders = []; fluid.each(fluid.resourceLoader.loaders, function (loader, key) { if (fluid.isValue(resourceSpec[key])) { loaders.push({ loader: loader, pathKey: key }); } }); if (loaders.length === 0) { loaders.push({ loader: fluid.resourceLoader.loaders.noLoader }); } else if (loaders.length > 1) { fluid.fail("Resource spec ", fluid.resourceLoader.sanitizeResourceSpec(resourceSpec), " is ambiguous because it has fields for multiple resource loaders filled out: at most one of the fields ", fluid.getMembers(loaders, "pathKey"), " can be used"); } return loaders[0]; }; /** A no-op `OneResourceLoader` which simply returns a pre-specified `resourceText`. Useful in the case the * real I/O has been done elsewhere and its results are simply to be relayed to another loader. * @param {ResourceSpec} resourceSpec - The `ResourceSpec` for which the `resourceText` field has already been filled in * @return {String} The `resourceSpec`'s `resourceText` member */ fluid.resourceLoader.loaders.resourceText = function (resourceSpec) { return resourceSpec.resourceText; }; fluid.resourceLoader.loaders.resourceText.noPath = true; /** A generalised 'promise' `OneResourceLoader` that allows some arbitrary asynchronous or synchronous process to be * interpolated into the loader. The function `promiseFunc` is invoked with * arguments `promiseArg` yielding a promise representing successful or unsuccessful loading of the resource value. * @param {ResourceSpec} resourceSpec - A `ResourceSpec` for which the `promiseFunc` field has already been filled in to hold * a function returning a promise * @return {Promise|Any} The result of invoking `promiseFunc` with `promiseArgs` - this function may also synchronously resolve * with a direct, non-promise value. */ fluid.resourceLoader.loaders.promiseFunc = function (resourceSpec) { var promiseFunc = fluid.event.resolveListener(resourceSpec.promiseFunc); return promiseFunc.apply(null, fluid.makeArray(resourceSpec.promiseArgs)); }; fluid.resourceLoader.loaders.promiseFunc.noPath = true; /** A `OneResourceLoader` which queries the `get` method of a DataSource in order to enact the required I/O * @param {ResourceSpec} resourceSpec - A `ResourceSpec` for which the `dataSource` field has already been filled in to hold * a reference to a `dataSource`, and perhaps also its `directModel` field. * @return {Promise} The resourceSpec's `promise` field */ fluid.resourceLoader.loaders.dataSource = function (resourceSpec) { fluid.getForComponent(resourceSpec.dataSource, "get"); return resourceSpec.dataSource.get(resourceSpec.directModel, resourceSpec.options); }; fluid.resourceLoader.loaders.dataSource.noPath = true; fluid.resourceLoader.loaders.dataSource.parsed = true; /** A placeholder resource which always immediately resolves with a structured message reporting that no resource was * configured, * @param {ResourceSpec} resourceSpec - A `ResourceSpec` for which none of the fields designating another `OneResourceLoader` * have been filled out * @return {Promise} An already resolved promise holding a structured message */ fluid.resourceLoader.loaders.noLoader = function (resourceSpec) { return fluid.promise().resolve({ noResource: true, message: ["No resource was configured for resource spec ", fluid.resourceLoader.sanitizeResourceSpec(resourceSpec), "; since it had none of the fields fields ", Object.keys(fluid.resourceLoader.loaders) + " filled out"] }); }; fluid.resourceLoader.loaders.noLoader.noPath = true; fluid.resourceLoader.loaders.noLoader.parsed = true; fluid.registerNamespace("fluid.resourceLoader.parsers"); /** A function accepting a fetched resource and parsing it into a more structured form. Given such a parser is * executed in an asynchronous chain, it should report failures as promise rejections rather than thrown exceptions. * @callback ResourceParser * @param {String} resourceText - The fetched value of the resource as a String * @return {Promise|Any} A parsed form of the resource */ /** Looks up a suitable parser based on an inspection of the contents of `fluid.resourceLoader.parsers` for an * implementation matching the `dataType` field in the supplied `resourceSpec`. If there is no such field or * the lookup fails, returns `fluid.identity` * @param {ResourceSpec} resourceSpec - The resourceSpec for which a parser is to be looked up * @return {ResourceParser} An appropriate parser for the resource's dataType, or `fluid.identity` if no such * parser is appropriate */ fluid.resourceLoader.resolveResourceParser = function (resourceSpec) { return !resourceSpec.loader.loader.parsed && fluid.resourceLoader.parsers[resourceSpec.dataType] || fluid.identity; }; /** Parses a fetched resource text as JSON * @param {String} resourceText - The text to be parsed * @return {Promise} A promise yielding the `resourceText` parsed as JSON, or else a rejection holding a readable * description of the location of the parse failure */ fluid.resourceLoader.parsers.json = function (resourceText) { return fluid.dataSource.parseJSON(resourceText); }; // Note: near-copy of fluid.invokersMergePolicy fluid.resourcesMergePolicy = function (target, source) { target = target || {}; fluid.each(source, function (oneResource, name) { if (!oneResource) { target[name] = oneResource; return; // Backwards compatibility for Infusion 2.x-style resources assumes that a string resource was meant to be a url } else if (fluid.isPrimitive(oneResource)) { oneResource = {url: oneResource}; } var oneR = target[name]; if (!oneR) { oneR = target[name] = {}; } for (var key in fluid.resourceLoader.loaders) { if (key in oneResource) { for (var key2 in fluid.resourceLoader.loaders) { delete oneR[key2]; } } } $.extend(oneR, oneResource); }); return target; }; /*** The top-level grade fluid.resourceLoader itself ***/ /** * A configurable component to allow users to load multiple resources by issuance of I/O. * The resources can be localised by means of options `locale`, `defaultLocale`. Once all * resources are loaded, the event `onResourcesLoaded` will be fired, which can be used * to time the creation of components dependent on the resources. In addition, any resources * requested during the construction of a component can be used to delay its construction until * they are consumed by some component workflow. */ fluid.defaults("fluid.resourceLoader", { gradeNames: ["fluid.modelComponent"], listeners: { /* On construction of the resourceLoader, kick off the process of fetching all the resources configured * within its resourceFetcher. Note that some or all of these resources may already have been fetched by * demands occuring during component startup (e.g. as initial model values or renderer templates), and so * the `onResourcesLoaded` event may fire immediately */ "onCreate.loadResources": "{that}.resourceFetcher.fetchAll", "onDestroy.destroyResourceEvents": "fluid.resourceLoader.destroyResourceEvents({that}.resourceFetcher)", "{that}.resourceFetcher.onInit": { namespace: "resourceLoaderCompletion", funcName: "fluid.resourceLoader.subscribeCompletion", args: ["{arguments}.0", "{that}"] }, "{that}.applier.earlyModelResolved": { funcName: "fluid.resourceLoader.modelUpdated", args: ["{that}.resourceFetcher", "{arguments}.0.resourceLoader", true] } }, modelListeners: { resourceLoader: { namespace: "resourceLoader", funcName: "fluid.resourceLoader.modelUpdated", excludeSource: "init", args: ["{that}.resourceFetcher", "{change}.value"] } }, mergePolicy: { resources: fluid.resourcesMergePolicy }, members: { resourceFetcher: { expander: { funcName: "fluid.resourceLoader.makeResourceFetcher", args: ["{that}", "{that}.options.resources", "{that}.options.resourceOptions", "{that}.transformResourceURL"] } }/*, // If these are demanded early, they will be resolved as instances of FetchOne by means of the framework's fluid.resourceFromRecord record mounter, // but are then overwritten by the resolved resourceSpec value by the noteComponentResource listener to the resource's transformEvent resources: {} */ }, resourceOptions: { // defaultLocale: "en", // May be supplied by integrators // locale: "en", // May be supplied by integrators // dataType: "json" // May be supplied by integrators terms: {} // May be supplied by integrators }, resources: {}, // Must be supplied by integrators invokers: { transformResourceURL: { funcName: "fluid.stringTemplate", args: ["{arguments}.0", "{that}.options.resourceOptions.terms"] } }, events: { onResourcesLoaded: null, onResourceError: null } }); /** Constructs a `fluid.resourceLoader' component's own `resourceFetcher` machine. Given that component options * are immutable, it takes a copy of the supplied `resourceSpecs` option (taken from the `resources` top-level * component option) before passing them to the fetcher's mutable copy at `resourceFetcher.resourceSpecs` * @param {fluid.resourceLoader} that - The resourceLoader component for which the fetcher is to be constructed * (currently used to target the delivery of the delivered `that.resources` members, and relay resource errors) * @param {ResourceSpecs} resourceSpecs - The resourceSpecs structure held in `options.resources` of the * @param {ResourceFetcherOptions} userResourceOptions - Options governing the entire resource fetcher (currently * `locale`, `defaultLocale` and `terms`) * @param {Function} transformResourceURL - A function {String -> String} which maps URL/path entries in resource * specs, possibly by interpolating term values * @return {ResourceFetcher} The ResourceFetcher ready to be attached to the ResourceLoader's top level */ fluid.resourceLoader.makeResourceFetcher = function (that, resourceSpecs, userResourceOptions, transformResourceURL) { var resourceOptions = $.extend({ ownerComponentId: that.id, // For debuggability ownerComponentPath: fluid.pathForComponent(that), onInitListeners: [[function (completionPromise) { fluid.resourceLoader.subscribeCompletion(completionPromise, that); }, "resourceLoaderCompletion"]] }, userResourceOptions); var fetcher = fluid.makeResourceFetcher(resourceSpecs, null, resourceOptions, transformResourceURL); fluid.each(fetcher.resourceSpecs, function (resourceSpec, key) { resourceSpec.transformEvent.addListener(function (parsed) { that.resources[key] = resourceSpec; return parsed; }, "noteComponentResource", "after:parsed"); resourceSpec.onError.addListener(that.events.onResourceError.fire); }); return fetcher; }; /* Subscribe for completion of a complete fetch of resources from the resourceLoader * @param {Promise} completionPromise - The completion promise to be subscribed to * @param {fluid.resourceLoader} that - The loader for which the I/O fetch process is to be started */ fluid.resourceLoader.subscribeCompletion = function (completionPromise, resourceLoader) { var fire = function () { resourceLoader.events.onResourcesLoaded.fire(resourceLoader.resourceFetcher.resourceSpecs); }; completionPromise.then(function () { // Special-case timings for FLUID-6588. Rather than fix up createOnEvent so that it can be used for events before onCreate, we just // enlaten this event so that it fires after onCreate. We mean to abolish createOnEvent components and so there is no point making them // any more usable. Note that would be hugely helpful at this point to either hav