Source: engine/ur/Migration.js

(function() {
    /**
     * Selector Migration handling for UR harmonization
     * hotspots previously assigned in same-origin-iframe applications can be migrated in cross-origin-iframe applications
     * or forced to be migrated using Help4.Feature.MigrateUrInSameOrigin
     *
     * - collect selectors to be migrated
     * - send migration request and handle response from application
     * - keep track of migrated selectors
     */
    Help4.engine.ur.Migration = class {
        /**
         *
         * @param {Help4.engine.ur.UrHarmonizationEngine} engine
         * @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selector
         * @returns {?Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse}
         */
        static selectorToUrHotspot(engine, selector) {
            return engine._migratedHotspots.find(hotspot => _selectorsEqual(selector, hotspot.selector));
        }

        /**
         * Add a selector to the list of selectors to be migrated
         * @param {Help4.engine.ur.UrHarmonizationEngine} engine
         * @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selector
         * @returns {boolean} - true if selector was added, false if it already existed
         */
        static addSelector(engine, selector) {
            const {
                /** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} */ _migratedHotspots
            } = engine;

            if (_migratedHotspots.some(hotspot => _selectorsEqual(selector, hotspot.selector))) {
                return false;
            }

            // reconstructed selector to make sure only rule and value are used
            const {rule, value} = selector;
            const selectorString = Help4.JSON.stringify({rule, value});
            _migratedHotspots.push(/** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse} */ {selector, selectorString});
            return true;
        }

        /**
         * Request migration of selectors previously recorded in this context / app frame
         * The app is expected to send v2.fromSelector message containing either be hotspotIds, if the selector maps
         * to an existing help element, or the current position of the element found using the selector.
         * @param {Help4.engine.ur.UrHarmonizationEngine} engine
         */
        static async requestSelectorMigration(engine) {
            const {
                _appFrame,
                _appType,
                /** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} */ _migratedHotspots,
            } = engine;
            // only send migration request for HTMLGUI and WDA
            const {HTMLGUI, WDA} = Help4.engine.ur.Connection.APP_TYPE;
            if (_appType !== HTMLGUI && _appType !== WDA) return;

            // check if there are any selectors to migrate
            const {contentWindow} = _appFrame || {};
            if (!contentWindow || !_migratedHotspots.length) return;

            // check frame for same origin and if migration is enabled for same origin
            const {MigrateUrInSameOrigin} = Help4.Feature;
            const {IFrameService} = Help4.service.recording;
            if (!MigrateUrInSameOrigin && IFrameService.isSameOriginWindow(contentWindow)) return;

            const {
                UrHarmonizationEngine: {MSG_TYPE: {v2}},
                Connection
            } = Help4.engine.ur;

            /** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest[]} */
            const selectors = [];
            for (const {hotspotId, selectorString} of _migratedHotspots) {
                // anything without hotspotId needs an update
                if (!hotspotId) selectors.push(selectorString);
            }

            if (selectors.length === 0) return;

            engine._log?.('CMP: sending MigrateSelectors', undefined, () => selectors);
            contentWindow.postMessage(Help4.JSON.stringify({
                service: v2.migrate,
                type: 'request',
                body: {selectors}
            }), Connection.getTargetOrigin(engine));
        }

        /**
         * Update map of migrated selectors
         * @param {Help4.engine.ur.UrHarmonizationEngine} engine
         * @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} migrated
         */
        static handleMigration(engine, {migrated}) {
            const {Update} = Help4.engine.ur;
            let structural = false;
            let moved = false;

            // update engine
            for (const hotspot of /** @type {Help4.engine.ur.UrHarmonizationEngine.UrMigrationResponse[]} */ engine._migratedHotspots) {
                const {position, hotspotId, selector} = hotspot;
                const {
                    hotspotId: newId,
                    position: newPosition
                } = migrated.find(m => _selectorsEqual(m.selector, selector)) || {};

                if (position) {
                    if (!Update.positionsEqual(position, newPosition)) {
                        moved ||= true;
                    }
                } else if ((!position && !hotspotId) || (!newPosition && !newId)) { // new or removed hotspot
                    structural ||= true;
                }
                hotspot.position = newPosition || null;
                hotspot.hotspotId = newId || null;
            }

            // update view
            engine._log?.('CMP: update after handleMigration', undefined, ()=>({structural, moved}));
            engine._sendUpdateNotification({structural, moved});
        }
    }

    /**
     * @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selectorA
     * @param {Help4.engine.ur.UrHarmonizationEngine.UrMigrationRequest} selectorB
     * @return {boolean}
     * @private
     */
    function _selectorsEqual({rule, value}, {rule: r, value: v}) {
        return rule === r && value === v;
    }
})();