@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
240 lines • 11 kB
JavaScript
/*
* 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
;