import { MetricsConfig } from '@amp-metrics/mt-client-config'; import { reflect } from '@amp-metrics/mt-metricskit-utils-private'; /* * src/delegate.js * mt-metricskit-delegates-core * * Copyright © 2022 Apple Inc. All rights reserved. * */ /** * Abstract class for delegates. * All delegate implementations should extend from this class. * @param {String} topic - Defines the AMP Analytics "topic" for events to be stored under * @param {Object} platformImpls - A map that include the platform-based components. * @param {Environment} platformImpls.environment - An Environment that provide the event field accessor for the platform * @param {EventRecorder} platformImpls.eventRecorder - An EventRecorder that provide the EventRecorder implementation for the platform * @constructor */ var Delegates = function Delegates(topic, platformImpls) { if (!reflect.isDefinedNonNullNonEmpty(topic) || !reflect.isString(topic)) { throw new Error('No valid topic was provided to Delegates.'); } this.config = this.getOrCreateConfig(topic); // TODO change platformImpls as an required argument in the next major version or supporting Typescript // since every platforms should provide their own impl for the Environment and EventHandler to the Delegates. if (reflect.isDefinedNonNull(platformImpls)) { this.environment = platformImpls.environment; this.eventRecorder = platformImpls.eventRecorder; } // A flag to indicate whether using the original execution context when calling the delegate methods. Default is false // TODO Consider to change this to true by default in the next major release // because using the execution context of the delegate methods may not necessary as accessing the the delegate methods' execution context is straightforward. this._useOrginalContextForDelegateFunc = false; }; /** * Initializes the delegate by setting up the config. * @param delegates * @return {Promise} */ Delegates.prototype.init = function () { if (!reflect.isDefinedNonNull(this.environment)) { throw new Error('No environment was provided to Delegate options.'); } if (!reflect.isDefinedNonNull(this.eventRecorder)) { throw new Error('No eventRecorder was provided to Delegate options.'); } this.config.environment.setDelegate(this.environment); this.config.logger.setDelegate(this.logger); this.config.network.setDelegate(this.network); var configSources = reflect.isFunction(this.configSources) ? this.configSources.bind(this) : null; return this.config.init(configSources); }; /** * Merge the provided delegates into the current Delegate. * NOTE: Any delegates which already exist in the Delegate won't be merged. * @param {Object} delegates - A key/value map of the system delegates */ Delegates.prototype.mergeDelegates = function (delegates) { var self = this; for (var key in delegates) { if (!self[key]) { self[key] = delegates[key]; } } this.config.setDelegate(delegates.config); }; /** * Cleans up resources used by the delegate. */ Delegates.prototype.cleanup = function cleanup() { if (reflect.isFunction(this.eventRecorder.cleanup)) { this.eventRecorder.cleanup(); } this.config = null; this.eventRecorder = null; this.environment = null; }; /** * Overrides the platform-specific event recorder for the Delegate. * @param {object} eventRecorderDelegates * @returns {Delegates} */ Delegates.prototype.setEventRecorder = function setEventRecorder(eventRecorderDelegates) { if (reflect.isDefinedNonNull(eventRecorderDelegates)) { if (!reflect.isDefinedNonNull(this.eventRecorder)) { this.eventRecorder = eventRecorderDelegates; } else { reflect.setDelegates(this.eventRecorder, eventRecorderDelegates); this.eventRecorder.setDelegate(eventRecorderDelegates); } } return this; }; /** * Overrides the existing environment implementations. * @param {object} environment * @return {Delegates} */ Delegates.prototype.setEnvironment = function setEnvironment(environment) { if (!reflect.isDefinedNonNull(this.environment)) { this.environment = environment; } else { var newEnvironment = Object.create(this.environment); reflect.extend(newEnvironment, environment); this.environment = newEnvironment; } return this; }; /** * Overrides the config methods * @param config * @return {Delegates} */ Delegates.prototype.setConfig = function setConfig(config) { this.config.setDelegate(config); return this; }; /** * Access the config instance from the child delegates before/during calling Delegates.constructor, * this method will create new config instance and set it to the Delegates instance and return the config when the config doesn't exist * @param {String} topic - The topic to create the metrics config instance * @return {MetricsConfig} */ Delegates.prototype.getOrCreateConfig = function getOrCreateConfig(topic) { if (!reflect.isDefinedNonNull(this.config)) { this.config = new MetricsConfig(topic); } return this.config; }; /** * Retrieves the config sources. This method must be implemented by subdelegates. * @abstract * @return {Promise} */ Delegates.prototype.configSources = function configSources() { throw new Error('This method should be implemented by subdelegates.'); }; /* * src/abstract_event_recorder.js * mt-metricskit-delegates-core * * Copyright © 2022 Apple Inc. All rights reserved. * */ /** * An abstract event recorder to provide common logic across platform event recorders * @constructor */ function AbstractEventRecorder() { this._operationPromiseChain = Promise.resolve(); } /** * Allows replacement of one or more of this class' functions * Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method. * To replace *all* methods of his class, simply have your delegate implement all the methods of this class * Your delegate can be a true object instance, an anonymous object, or a class object. * Your delegate is free to have as many additional non-matching methods as it likes. * It can even act as a delegate for multiple MetricsKit objects, though that is not recommended. * * "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions. * This allows callers to use "canned" delegates to get most of their functionality, but still replace some number of methods that need custom implementations. * If, for example, a client wants to use the "canned" itml/environment delegate with the exception of, say, the "appVersion" method, they can set itml/environment as the delegate, and * then call "setDelegate()" again with their own delegate containing only a single method of "appVersion" as the delegate, which would leave all the other "replaced" methods intact, * but override the "appVersion" method again, this time with their own supplied delegate. * * NOTE: when the delegate function is called, it will include an additional final parameter representing the original function that it replaced (the callee would typically name this parameter "replacedFunction"). * This allows the delegate to, essentially, call "super" before or after it does some work. * If a replaced method is overridden again with a subsequent "setDelegate()" call, the "replacedFunction" parameter will be the previous delegate. * @example: * To override one or more methods, in place: * eventRecorder.setDelegate({recordEvent: itms.recordEvent}); * To override one or more methods with a separate object: * eventRecorder.setDelegate(eventRecorderDelegate); * (where "eventRecorderDelegate" might be defined elsewhere as, e.g.: * var eventRecorderDelegate = {recordEvent: itms.recordEvent, * sendMethod: 'itms'}; * To override one or more methods with an instantiated object from a class definition: * eventRecorder.setDelegate(new EventRecorderDelegate()); * (where "EventRecorderDelegate" might be defined elsewhere as, e.g.: * function EventRecorderDelegate() { * } * EventRecorderDelegate.prototype.recordEvent = itms.recordEvent; * EventRecorderDelegate.prototype.sendMethod = function sendMethod() { * return 'itms'; * }; * To override one or more methods with a class object (with "static" methods): * eventRecorder.setDelegate(EventRecorderDelegate); * (where "EventRecorderDelegate" might be defined elsewhere as, e.g.: * function EventRecorderDelegate() { * } * EventRecorderDelegate.recordEvent = itms.recordEvent; * EventRecorderDelegate.sendMethod = function sendMethod() { * return 'itms'; * }; * @param {Object} Object or Class with delegate method(s) to be called instead of default (built-in) methods. * @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object, * otherwise returns false. */ AbstractEventRecorder.prototype.setDelegate = function setDelegate(delegate) { return reflect.attachDelegate(this, delegate); }; /** * Public method to interact with Processors to record an event * @param {String} topic - an 'override' topic which will override the main topic. * @param {Promise|Object} eventFields - a Promise/JavaScript object which will be converted to a JSON string and sent to AMP Analytics. * @return {Promise} */ AbstractEventRecorder.prototype.recordEvent = function recordEvent(topic, eventFields) { var vargs = Array.prototype.slice.call(arguments, 2); var self = this; this._operationPromiseChain = this._operationPromiseChain.then(function () { return Promise.resolve(eventFields).then(function (eventFields) { return self._recordEvent.apply(self, [topic, eventFields].concat(vargs)); }); }); return this._operationPromiseChain; }; /** * Sends any remaining events in the queue, then clears it * This is typically called on page / app close when the JS context is about to disappear and thus we will * not know if the events made it to the server * @param {Boolean} appIsExiting - Pass true if events are being flushed due to your app exiting or page going away * (the send method will be different in order to attempt to post events prior to actual termination) * @param {String} appExitSendMethod (optional) the send method for how events will be flushed when the app is exiting. * Possible options are enumerated in the `eventRecorder.SEND_METHOD` object. * Note: This argument will be ignored if appIsExiting is false. * @returns {Promise} */ AbstractEventRecorder.prototype.flushUnreportedEvents = function flushUnreportedEvents() { var args = Array.prototype.slice.call(arguments); var self = this; return this._operationPromiseChain.then(function () { // Reset the promise chain self._operationPromiseChain = Promise.resolve(); return self._flushUnreportedEvents.apply(self, args); }); }; /** * Abstract recordEvent method * Subclasses implement this method to handle how to record an event * @abstract * @protected * @param {String} topic - an 'override' topic which will override the main topic. * @param {Object} eventFields - a JavaScript object which will be converted to a JSON string and sent to AMP Analytics. * @returns {Promise} */ AbstractEventRecorder.prototype._recordEvent = function _recordEvent(topic, eventFields) {}; /** * Abstract flushUnreportedEvents method * Subclasses implement this method to handle how to flush the cache events * @abstract * @protected * @param {Boolean} appIsExiting - if events are being flushed due to your app exiting (or page going away for web-apps), pass "true". * This allows MetricsKit to modify its flush strategy to attempt to post events prior to actual termination. * In cases where appIsExiting==false, the parameter may be omitted. * @returns {Promise} */ AbstractEventRecorder.prototype._flushUnreportedEvents = function _flushUnreportedEvents(appIsExiting) {}; export { AbstractEventRecorder, Delegates };