UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

240 lines 11 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.cloneUniqueListeners = exports.Lifecycle = void 0; const semver_1 = require("semver"); // needed for TS to not put everything inside /lib/src // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const pjson = __importStar(require("../package.json")); const logger_1 = require("./logger/logger"); /** * An asynchronous event listener and emitter that follows the singleton pattern. The singleton pattern allows lifecycle * events to be emitted from deep within a library and still be consumed by any other library or tool. It allows other * developers to react to certain situations or events in your library without them having to manually call the method themselves. * * An example might be transforming metadata before it is deployed to an environment. As long as an event was emitted from the * deploy library and you were listening on that event in the same process, you could transform the metadata before the deploy * regardless of where in the code that metadata was initiated. * * @example * ``` * // Listen for an event in a plugin hook * Lifecycle.getInstance().on('deploy-metadata', transformMetadata) * * // Deep in the deploy code, fire the event for all libraries and plugins to hear. * Lifecycle.getInstance().emit('deploy-metadata', metadataToBeDeployed); * * // if you don't need to await anything * use `void Lifecycle.getInstance().emit('deploy-metadata', metadataToBeDeployed)` ; * ``` */ class Lifecycle { listeners; uniqueListeners; static telemetryEventName = 'telemetry'; static warningEventName = 'warning'; logger; constructor(listeners = {}, uniqueListeners = new Map()) { this.listeners = listeners; this.uniqueListeners = uniqueListeners; } /** * return the package.json version of the sfdx-core library. */ static staticVersion() { return pjson.version; } /** * Retrieve the singleton instance of this class so that all listeners and emitters can interact from any library or tool */ static getInstance() { // Across a npm dependency tree, there may be a LOT of versions of `@salesforce/core`. We want to ensure that consumers are notified when // listening on a lifecycle event that is fired by a different version of `@salesforce/core`. Adding the instance on the global object will // ensure this. // // For example, a consumer calls `Lifecycle.getInstance().on('myEvent', ...)` on version `@salesforce/core@2.12.2`, and another consumer calls // `Lifecycle.getInstance().emit('myEvent', ...)` on version `@salesforce/core@2.13.0`, the on handler will never be called. // // Note: If ANYTHING is ever added to this class, it needs to check and update `global.salesforceCoreLifecycle` to the newer version. // One way this can be done by adding a `version = require(../package.json).version` to the Lifecycle class, then checking if // `global.salesforceCoreLifecycle` is greater or equal to that version. // // For example, let's say a new method is added in `@salesforce/core@3.0.0`. If `Lifecycle.getInstance()` is called fist by // `@salesforce/core@2.12.2` then by someone who depends on version `@salesforce/core@3.0.0` (who depends on the new method) // they will get a "method does not exist on object" error because the instance on the global object will be of `@salesforce/core@2.12.2`. // // Nothing should EVER be removed, even across major versions. if (!global.salesforceCoreLifecycle) { // it's not been loaded yet (basic singleton pattern) global.salesforceCoreLifecycle = new Lifecycle(); } else if ( // an older version was loaded that should be replaced (0, semver_1.compare)(global.salesforceCoreLifecycle.version(), Lifecycle.staticVersion()) === -1) { const oldInstance = global.salesforceCoreLifecycle; // use the newer version and transfer any listeners from the old version // object spread and the clone fn keep them from being references global.salesforceCoreLifecycle = new Lifecycle({ ...oldInstance.listeners }, (0, exports.cloneUniqueListeners)(oldInstance.uniqueListeners)); // clean up any listeners on the old version Object.keys(oldInstance.listeners).map((eventName) => { oldInstance.removeAllListeners(eventName); }); } return global.salesforceCoreLifecycle; } /** * return the package.json version of the sfdx-core library. */ // eslint-disable-next-line class-methods-use-this version() { return pjson.version; } /** * Remove all listeners for a given event * * @param eventName The name of the event to remove listeners of */ removeAllListeners(eventName) { this.listeners[eventName] = []; this.uniqueListeners.delete(eventName); } /** * Get an array of listeners (callback functions) for a given event * * @param eventName The name of the event to get listeners of */ getListeners(eventName) { const listeners = this.listeners[eventName]?.concat(Array.from((this.uniqueListeners.get(eventName) ?? []).values()) ?? []); if (listeners) { return listeners; } else { this.listeners[eventName] = []; return []; } } /** * Create a listener for the `telemetry` event * * @param cb The callback function to run when the event is emitted */ onTelemetry(cb) { this.on(Lifecycle.telemetryEventName, cb); } /** * Create a listener for the `warning` event * * @param cb The callback function to run when the event is emitted */ onWarning(cb) { this.on(Lifecycle.warningEventName, cb); } /** * Create a new listener for a given event * * @param eventName The name of the event that is being listened for * @param cb The callback function to run when the event is emitted * @param uniqueListenerIdentifier A unique identifier for the listener. If a listener with the same identifier is already registered, a new one will not be added */ on(eventName, cb, uniqueListenerIdentifier) { const listeners = this.getListeners(eventName); if (listeners.length !== 0) { if (!this.logger) { this.logger = logger_1.Logger.childFromRoot('Lifecycle'); } this.logger.debug(`${listeners.length + 1} lifecycle events with the name ${eventName} have now been registered. When this event is emitted all ${listeners.length + 1} listeners will fire.`); } if (uniqueListenerIdentifier) { if (!this.uniqueListeners.has(eventName)) { // nobody is listening to the event yet this.uniqueListeners.set(eventName, new Map([[uniqueListenerIdentifier, cb]])); } else if (!this.uniqueListeners.get(eventName)?.has(uniqueListenerIdentifier)) { // the unique listener identifier is not already registered this.uniqueListeners.get(eventName)?.set(uniqueListenerIdentifier, cb); } } else { listeners.push(cb); this.listeners[eventName] = listeners; } } /** * Emit a `telemetry` event, causing all callback functions to be run in the order they were registered * * @param data The data to emit */ async emitTelemetry(data) { return this.emit(Lifecycle.telemetryEventName, data); } /** * Emit a `warning` event, causing all callback functions to be run in the order they were registered * * @param data The warning (string) to emit */ async emitWarning(warning) { // if there are no listeners, warnings should go to the node process so they're not lost // this also preserves behavior in UT where there's a spy on process.emitWarning if (this.getListeners(Lifecycle.warningEventName).length === 0) { process.emitWarning(warning); } return this.emit(Lifecycle.warningEventName, warning); } /** * Emit a given event, causing all callback functions to be run in the order they were registered * * @param eventName The name of the event to emit * @param data The argument to be passed to the callback function */ async emit(eventName, data) { const listeners = this.getListeners(eventName); if (listeners.length === 0 && eventName !== Lifecycle.warningEventName) { if (!this.logger) { this.logger = logger_1.Logger.childFromRoot('Lifecycle'); } this.logger.debug(`A lifecycle event with the name ${eventName} does not exist. An event must be registered before it can be emitted.`); } else { for (const cb of listeners) { // eslint-disable-next-line no-await-in-loop await cb(data); } } } } exports.Lifecycle = Lifecycle; const cloneListeners = (listeners) => new Map(Array.from(listeners.entries())); const cloneUniqueListeners = (uniqueListeners) => // in case we're crossing major sfdx-core versions where uniqueListeners might be undefined new Map(Array.from(uniqueListeners?.entries() ?? []).map(([key, value]) => [key, cloneListeners(value)])); exports.cloneUniqueListeners = cloneUniqueListeners; //# sourceMappingURL=lifecycleEvents.js.map