<commit_before>var imageTagBuilder = function() {

	// Initialize
  var that = {};
  var attributeWidth, sourceWidth, sourceHeight;

  // Set defaults
  that.innerWidth = window.innerWidth;
  that.innerHeight = window.innerHeight;
  that.dpr = window.devicePixelRatio;

  // Define methods

  that.attributeHeight = function() {
    return attributeWidth * that.sourceHeight() / that.sourceWidth();
  };

  that.attributeWidth = function() {
  	return attributeWidth;
  };

  that.imageTag = function() {
  	return '';
  };

  that.prep = function(params) {
    that.requestWidth(800);
  };

  that.requestWidth = function(width) {
    attributeWidth = width;
  };

  that.setSourceHeight = function(height) {
  	sourceHeight = height;
  }

  that.setSourceWidth = function(width) {
    sourceWidth = width;
  }

  that.sourceHeight = function() {
  	return sourceHeight;
  }

  that.sourceWidth = function() {
    return sourceWidth ;
  }

  return that;
}

<commit_msg>Using setter for attributeWidth.<commit_after>var imageTagBuilder = function() {

	// Initialize
  var that = {};
  var attributeWidth, sourceWidth, sourceHeight;

  // Set defaults
  that.innerWidth = window.innerWidth;
  that.innerHeight = window.innerHeight;
  that.dpr = window.devicePixelRatio;

  // Define methods

  that.attributeHeight = function() {
    return attributeWidth * that.sourceHeight() / that.sourceWidth();
  };

  that.attributeWidth = function() {
  	return attributeWidth;
  };

  that.imageTag = function() {
  	return '';
  };

  that.prep = function(params) {
    that.requestWidth(800);
  };

  that.requestWidth = function(width) {
  	that.setAttributeWidth(width);
  };

  that.setAttributeWidth= function(width) {
    attributeWidth = width;
  };

  that.setSourceHeight = function(height) {
  	sourceHeight = height;
  }

  that.setSourceWidth = function(width) {
    sourceWidth = width;
  }

  that.sourceHeight = function() {
  	return sourceHeight;
  }

  that.sourceWidth = function() {
    return sourceWidth ;
  }

  return that;
}

<|endoftext|>


<commit_before>(function() {
'use strict';

angular.module('superdesk.editor2.embed', []).controller('SdAddEmbedController', SdAddEmbedController);

SdAddEmbedController.$inject = ['embedService', '$element', '$timeout', '$q', 'lodash', 'EMBED_PROVIDERS', '$scope', 'editor', '$http'];
function SdAddEmbedController (embedService, $element, $timeout, $q, _, EMBED_PROVIDERS, $scope, editor, $http) {
    var vm = this;
    angular.extend(vm, {
        editorCtrl: undefined,  // defined in link method
        previewLoading: false,
        toggle: function(close) {
            // use parameter or toggle
            vm.extended = angular.isDefined(close) ? !close : !vm.extended;
        },
        /**
         * Return html code to represent an embedded link
         *
         * @param {string} url
         * @param {string} title
         * @param {string} description
         * @param {string} illustration
         * @return {string} html
         */
        linkToHtml: function(url, title, description, illustration) {
            var html = [
                '
',
                angular.isDefined(illustration) ?
                '  ' : '',
                '
',
                '      ' + title + '',
                '
',
                '
' + description + '
',
                '
'];
            return html.join('\n');
        },
        retrieveEmbed:function() {
            function retrieveEmbedFromUrl() {
                return embedService.get(vm.input).then(function(data) {
                    var embed = data.html;
                    if (!angular.isDefined(embed)) {
                        if (data.type === 'link') {
                            embed = vm.linkToHtml(data.url, data.title, data.description, data.thumbnail_url);
                        } else {
                            embed = editor.generateImageTag({url: data.url, altText: data.description});
                        }
                    }
                    return $q.when(embed).then(function(embed) {
                        return {
                            body: embed,
                            provider: data.provider_name || EMBED_PROVIDERS.custom
                        };
                    });
                });
            }
            function parseRawEmbedCode() {
                var waitFor = [];
                var embedBlock = {
                    body: vm.input,
                    provider: EMBED_PROVIDERS.custom
                };
                var providersKnown = [
                    {
                        pattern: /twitter\.com\/widgets\.js/g,
                        name: EMBED_PROVIDERS.twitter
                    },
                    {
                        pattern: /www\.youtube\.com/g,
                        name: EMBED_PROVIDERS.youtube
                    },
                    {
                        pattern: /src="(.*vidible\.tv.*pid=.*.js)/g,
                        name: EMBED_PROVIDERS.vidible,
                        callback: function(match) {
                            var url = 'https://' + match[1];
                            // $http raise a Same Origin Policy error, so we use jquery here
                            waitFor.push(
                                $q(function(resolve, reject) {
                                    $.ajax({
                                        url: url,
                                        type: 'GET',
                                        dataType: 'text',
                                        success: function(data) {
                                            data = /({.*})/.exec(data)[1];
                                            data = JSON.parse(data);
                                            // set association
                                            embedBlock.association = {
                                                uri: data.bid.id,
                                                title: data.bid.videos[0].name,
                                                type: 'video',
                                                thumbnail: data.bid.videos[0].thumbnail,
                                                url: data.bid.videos[0].videoUrls[0],
                                                // size: ,
                                                // creationDate: ,
                                                company: data.bid.videos[0].studioName,
                                                duration: data.bid.videos[0].metadata.duration,
                                            };
                                            resolve(embedBlock);
                                        }
                                    });
                                })
                            );
                        }
                    }
                ];
                // try to guess the provider of the custom embed
                for (var i = 0; i < providersKnown.length; i++) {
                    var provider = providersKnown[i];
                    var match = provider.pattern.exec(vm.input);
                    if (match) {
                        embedBlock.provider = provider.name;
                        if (provider.callback) {
                            provider.callback(match);
                        }
                        break;
                    }
                }
                return $q.all(waitFor).then(function() {
                    return embedBlock;
                });
            }
            var embedCode;
            // if it's an url, use embedService to retrieve the embed code
            if (_.startsWith(vm.input, 'http')) {
                embedCode = retrieveEmbedFromUrl(vm.input);
            // otherwise we use the content of the field directly
            } else {
                embedCode = parseRawEmbedCode(vm.input);
            }
            return $q.when(embedCode);
        },
        updatePreview: function() {
            vm.previewLoading = true;
            vm.retrieveEmbed().then(function(embed) {
                angular.element($element).find('.preview').html(embed.body.replace('\\n', ''));
                vm.previewLoading = false;
            });
        },
        createFigureBlock: function(data) {
            // create a new block containing the embed
            data.blockType = 'embed';
            return vm.editorCtrl.insertNewBlock(vm.addToPosition, data);
        },
        createBlockFromEmbed: function() {
            vm.retrieveEmbed().then(function(embed) {
                vm.createFigureBlock({
                    embedType: embed.provider,
                    body: embed.body,
                    association: embed.association
                });
                // close the addEmbed form
                vm.toggle(true);
            });
        },
        closeEmbed: function() {
            //put block back together on embed form close.
            vm.toggle();
        }
    });

    // toggle when the `extended` directive attribute changes
    $scope.$watch(function() {
        return vm.extended;
    }, function(extended, wasExtended) {
        // on enter, focus on input
        if (angular.isDefined(extended)) {
            if (extended) {
                $timeout(function() {
                    angular.element($element).find('input').focus();
                }, 500, false); // positive timeout because of a chrome issue
            // on leave, clear field
            } else if (wasExtended) {
                vm.input = '';
                if (typeof vm.onClose === 'function') {
                    $timeout(vm.onClose);
                }
            }
        }
    });
}
})();
<commit_msg>Vidible: use the new custom endpoint to fetch video metadata<commit_after>(function() {
'use strict';

angular.module('superdesk.editor2.embed', []).controller('SdAddEmbedController', SdAddEmbedController);

SdAddEmbedController.$inject = ['embedService', '$element', '$timeout', '$q', 'lodash',
'EMBED_PROVIDERS', '$scope', 'editor', 'api'];
function SdAddEmbedController (embedService, $element, $timeout, $q, _,
EMBED_PROVIDERS, $scope, editor, api) {
    var vm = this;
    angular.extend(vm, {
        editorCtrl: undefined,  // defined in link method
        previewLoading: false,
        toggle: function(close) {
            // use parameter or toggle
            vm.extended = angular.isDefined(close) ? !close : !vm.extended;
        },
        /**
         * Return html code to represent an embedded link
         *
         * @param {string} url
         * @param {string} title
         * @param {string} description
         * @param {string} illustration
         * @return {string} html
         */
        linkToHtml: function(url, title, description, illustration) {
            var html = [
                '
',
                angular.isDefined(illustration) ?
                '  ' : '',
                '
',
                '      ' + title + '',
                '
',
                '
' + description + '
',
                '
'];
            return html.join('\n');
        },
        retrieveEmbed:function() {
            function retrieveEmbedFromUrl() {
                return embedService.get(vm.input).then(function(data) {
                    var embed = data.html;
                    if (!angular.isDefined(embed)) {
                        if (data.type === 'link') {
                            embed = vm.linkToHtml(data.url, data.title, data.description, data.thumbnail_url);
                        } else {
                            embed = editor.generateImageTag({url: data.url, altText: data.description});
                        }
                    }
                    return $q.when(embed).then(function(embed) {
                        return {
                            body: embed,
                            provider: data.provider_name || EMBED_PROVIDERS.custom
                        };
                    });
                });
            }
            function parseRawEmbedCode() {
                var waitFor = [];
                var embedBlock = {
                    body: vm.input,
                    provider: EMBED_PROVIDERS.custom
                };
                var providersKnown = [
                    {
                        pattern: /twitter\.com\/widgets\.js/g,
                        name: EMBED_PROVIDERS.twitter
                    },
                    {
                        pattern: /www\.youtube\.com/g,
                        name: EMBED_PROVIDERS.youtube
                    },
                    {
                        pattern: /src=".*vidible\.tv.*pid=(.+)\/(.+).js/g,
                        name: EMBED_PROVIDERS.vidible,
                        callback: function(match) {
                            return api.get('vidible/bcid/' + match[2] + '/pid/' + match[1])
                            .then(function(data) {
                                embedBlock.association = data;
                            });
                        }
                    }
                ];
                // try to guess the provider of the custom embed
                for (var i = 0; i < providersKnown.length; i++) {
                    var provider = providersKnown[i];
                    var match = provider.pattern.exec(vm.input);
                    if (match) {
                        embedBlock.provider = provider.name;
                        if (provider.callback) {
                            waitFor.push(provider.callback(match));
                        }
                        break;
                    }
                }
                return $q.all(waitFor).then(function() {
                    return embedBlock;
                });
            }
            var embedCode;
            // if it's an url, use embedService to retrieve the embed code
            if (_.startsWith(vm.input, 'http')) {
                embedCode = retrieveEmbedFromUrl(vm.input);
            // otherwise we use the content of the field directly
            } else {
                embedCode = parseRawEmbedCode(vm.input);
            }
            return $q.when(embedCode);
        },
        updatePreview: function() {
            vm.previewLoading = true;
            vm.retrieveEmbed().then(function(embed) {
                angular.element($element).find('.preview').html(embed.body.replace('\\n', ''));
                vm.previewLoading = false;
            });
        },
        createFigureBlock: function(data) {
            // create a new block containing the embed
            data.blockType = 'embed';
            return vm.editorCtrl.insertNewBlock(vm.addToPosition, data);
        },
        createBlockFromEmbed: function() {
            vm.retrieveEmbed().then(function(embed) {
                vm.createFigureBlock({
                    embedType: embed.provider,
                    body: embed.body,
                    association: embed.association
                });
                // close the addEmbed form
                vm.toggle(true);
            });
        },
        closeEmbed: function() {
            //put block back together on embed form close.
            vm.toggle();
        }
    });

    // toggle when the `extended` directive attribute changes
    $scope.$watch(function() {
        return vm.extended;
    }, function(extended, wasExtended) {
        // on enter, focus on input
        if (angular.isDefined(extended)) {
            if (extended) {
                $timeout(function() {
                    angular.element($element).find('input').focus();
                }, 500, false); // positive timeout because of a chrome issue
            // on leave, clear field
            } else if (wasExtended) {
                vm.input = '';
                if (typeof vm.onClose === 'function') {
                    $timeout(vm.onClose);
                }
            }
        }
    });
}
})();
<|endoftext|>


<commit_before>/*jslint sloppy: true, browser: true */
/*global ArticleContentHandler, enyo, webos, moboreader */

var LIST_ITEM_HEIGHT = 50;

enyo.kind({
	name: "moboreader.MainView",
	kind: "FittableRows",
	fit: true,
	published: {
		pocketDL: 0,
		spritzDL: 0,
		articleCollection: "",
		dbActivities: 0
	},
	computed: {
		activity: ["pocketDL", "spritzDL", "dbActivities", {cached: true}]
	},
	activity: function () { return this.pocketDL || this.spritzDL || this.dbActivities; },
	bindings: [
		{from: "$.api.active", to: "pocketDL" },
		{from: "^.moboreader.Spritz.numDownloading", to: "spritzDL"},
		{from: "activity", to: "$.activitySpinner.showing"},
		{from: "articleCollection.length", to: "$.articleCount.content" },
		{from: "$.ArticleContentHandler.dbActivities", to: "dbActivities" }
	],
	components: [
		{
			kind: "onyx.Spinner",
			name: "activitySpinner",
			showing: false,
			style: "position: absolute; right: 5px; top: 5px; margin: 0px; background-color: rgb(80,80,80); border-radius: 15px; z-index: 100;"
		},
		{
			kind: "enyo.Panels",
			fit: true,
			draggable: false,
			classes: "enyo-fill",
			name: "MainPanels",
			components: [
				{
					name: "ArticleListPanel",
					kind: "FittableRows",
					classes: "enyo-fill enyo-fit",
					components: [
						{
							kind: "onyx.Toolbar",
							style: "text-align: center;",
							components: [
								{
									style: "text-align: center; margin: auto; float: left;",
									components: [
										{
											style: "font-size: 14px;",
											name: "articleCount",
											content: "0"
										},
										{
											style: "font-size: 10px;",
											content: "Articles"
										}
									]
								},
								{content: "Mobo Reader"}
							]
						},
						{
							kind: "enyo.DataList",
							ontap: "articleSelected",
							name: "articleList",
							fit: true,
							scrollerOptions: {
								horizontal: "hidden",
								touch: true
							},
							fixedChildSize: LIST_ITEM_HEIGHT,
							components: [
								{kind: "moboreader.ArticleListItem" }
							]
						},
						{
							kind: "onyx.MoreToolbar",
							components: [
								{
									kind: "onyx.Button",
									content: "Refresh",
									ontap: "refreshTap",
									onhold: "refreshHold"
								},
								{
									kind: "onyx.Button",
									content: "Add",
									ontap: "showAddDialog"
								},
								{
									kind: "onyx.Button",
									content: "Settings",
									ontap: "settingsTap"
								}
							]
						}
					]
				},
				{
					name: "ArticleViewPanel",
					classes: "enyo-fill enyo-fit",
					onBack: "handleBackGesture",
					components: [
						{kind: "moboreader.ArticleView", name: "articleView", classes: "enyo-fill enyo-fit", onBack: "handleBackGesture"}
					]
				}
			]
		},
		{
			kind: "onyx.Popup",
			name: "refreshPopup",
			components: [
				{
					classes: "popup-buttons",
					kind: "onyx.Button",
					content: "Delete & refresh",
					ontap: "forceRefreshTap"
				},
				{
					classes: "popup-buttons",
					kind: "onyx.Button",
					content: "Full refresh",
					ontap: "refreshFullSyncTap"
				},
				{
					classes: "popup-buttons",
					kind: "onyx.Button",
					content: "Fast refresh",
					ontap: "refreshTap"
				}
			]
		},

		//non ui stuff:
		{
			name: "api",
			kind: "moboreader.Api"
		},
		{
			name: "addDialog",
			kind: "moboreader.AddDialog",
			onAdd: "addArticle"
		},
		{
			name: "settingsDialog",
			kind: "moboreader.SettingsDialog",
			onLogoutRequest: "logoutOfPocket"
		},
		{
			kind: "Signals",
			onAddArticle: "addArticle",
			onrelaunch: "addArticle",
			onArticleOpReturned: "continueWipe",
			onunload: "cleanUpOnUnload",

			onNeedShowAuth: "showAuth",
			onAuthOk: "refreshTap",
			onHideAuth: "hideAuth"
		}
	],
	create: function () {
		this.inherited(arguments);

		function fetchResult() {
			this.log("api collection fetch successful.");
			//do this after fetch to prevent empty list in UI.
			this.$.articleView.setCollection(this.articleCollection);
			this.$.articleList.set("collection", this.articleCollection);
			setTimeout(this.refreshTap.bind(this, null, null, false), 1000);

			if (window.PalmSystem) {
				window.PalmSystem.stageReady();
				if (window.PalmSystem.allowResizeOnPositiveSpaceChange) {
					window.PalmSystem.allowResizeOnPositiveSpaceChange(false); //deactivate keyboard resizing our app.
				}
				this.log("Launch Params: " + JSON.stringify(webos.launchParams()));
				if (webos.launchParams().url) {
					this.log("Adding article: " + webos.launchParams().url);
					this.$.api.addArticle(webos.launchParams().url, this.articleCollection);
				}
			}
		}

		this.articleCollection = new moboreader.ArticleCollection({url: "pocket-unread-list"});
		this.articleCollection.set("url", "pocket-unread-list");
		this.articleCollection.fetch({strategy: "merge", success: fetchResult.bind(this), fail: fetchResult.bind(this)});
		this.$.articleView.setApi(this.$.api);
	},
	cleanUpOnUnload: function () {
		this.log("Cleaning up on unload.");
		this.articleCollection.storeWithChilds();
	},

	showAddDialog: function () {
		this.$.addDialog.doShow();
	},
	addArticle: function (inSender, inEvent) {
		/*jslint unparam:true*/
		if (!inEvent || !inEvent.url) {
			this.warn("No url supplied. Can't add article.");
		}
		this.$.api.addArticle(inEvent.url, this.articleCollection, inEvent.title);
		//this.resize();
	},

	refreshTap: function (inSender, inEvent, slowSync) {
		/*jslint unparam:true*/
		if (this.refreshHeld) {
			this.refreshHeld = false;
		} else {
			this.$.api.downloadArticles(this.articleCollection, slowSync);
			this.$.refreshPopup.hide();
		}
	},
	refreshFullSyncTap: function () {
		this.refreshTap(null, null, true);
		this.$.refreshPopup.hide();
	},
	refreshHold: function (inSender, inEvent) {
		this.refreshHeld = true;
		//show popup with more refresh options.
		this.$.refreshPopup.showAtEvent(inEvent);
	},
	forceRefreshTap: function () {
		this.articleCollection.whipe();
		this.dbOpId = ArticleContentHandler.wipe();
		this.$.refreshPopup.hide();
	},
	continueWipe: function (inSender, inEvent) {
		/*jslint unparam:true*/
		if (inEvent.activityId === this.dbOpId) {
			if (this.loggingOut) {
				this.$.api.logout();
				this.loggingOut = false;
			} else {
				this.$.api.downloadArticles(this.articleCollection, true);
			}
		}
	},
	settingsTap: function () {
		this.$.settingsDialog.show();
	},
	logoutOfPocket: function () {
		this.loggingOut = true;
		this.forceRefreshTap();
	},

	articleSelected: function (inSender, inEvent) {
		/*jslint unparam:true*/
		this.lastArticle = this.$.articleList.selected();
		if (this.lastArticle) {
			this.$.articleView.setArticleModel(this.lastArticle);
			this.$.MainPanels.setIndex(1);
			this.$.articleList.deselectAll();
		}
	},
	handleBackGesture: function () {
		if (this.$.MainPanels.getIndex() === 2) {
			this.log("In auth dialog, prevent back!");
			return;
		}

		this.$.MainPanels.setIndex(0);
		this.$.settingsDialog.hide();

		//this only works, because I hacked enyo/source/ui/data/VerticalDelegate.js scrollToIndex to always scroll to top, i.e. replaced line 291 wiht list.$.scroller.scrollIntoView(c, true);
		if (this.lastArticle) {
			var scrollTo = this.articleCollection.indexOf(this.lastArticle),
				listHeight = this.$.articleList.getBounds().height,
				numArticlesOnScreen = listHeight / LIST_ITEM_HEIGHT;

			scrollTo -= Math.round((numArticlesOnScreen - 1) / 2); //list will scroll to top, allow some articles above it to show up, too.

			if (scrollTo >= this.articleCollection.length) {
				scrollTo = this.articleCollection.length - 1;
			} else if (scrollTo < 0) {
				scrollTo = 0;
			}
			this.$.articleList.scrollToIndex(scrollTo);
		}
	},

	showAuth: function (inSender, inEvent) {
		if (!this.authCreated) {
			this.authCreated = true;
			var authDialog = this.$.MainPanels.createComponent({
				name: "AuthPanel",
				kind: "moboreader.AuthDialog"
			});
			this.$.MainPanels.render();
			//need to hand signal for auth dialog to catch it:
			authDialog.prepareAuthDialog(inSender, inEvent);
		}
		this.lastPanelIndex = this.$.MainPanels.getIndex();
		this.$.MainPanels.setIndex(2);
	},
	hideAuth: function () {
		if (this.$.MainPanels.getIndex() === 2) {
			this.$.MainPanels.setIndex(this.lastPanelIndex || 0);
		}
	}
});
<commit_msg>close popup after button press<commit_after>/*jslint sloppy: true, browser: true */
/*global ArticleContentHandler, enyo, webos, moboreader */

var LIST_ITEM_HEIGHT = 50;

enyo.kind({
	name: "moboreader.MainView",
	kind: "FittableRows",
	fit: true,
	published: {
		pocketDL: 0,
		spritzDL: 0,
		articleCollection: "",
		dbActivities: 0
	},
	computed: {
		activity: ["pocketDL", "spritzDL", "dbActivities", {cached: true}]
	},
	activity: function () { return this.pocketDL || this.spritzDL || this.dbActivities; },
	bindings: [
		{from: "$.api.active", to: "pocketDL" },
		{from: "^.moboreader.Spritz.numDownloading", to: "spritzDL"},
		{from: "activity", to: "$.activitySpinner.showing"},
		{from: "articleCollection.length", to: "$.articleCount.content" },
		{from: "$.ArticleContentHandler.dbActivities", to: "dbActivities" }
	],
	components: [
		{
			kind: "onyx.Spinner",
			name: "activitySpinner",
			showing: false,
			style: "position: absolute; right: 5px; top: 5px; margin: 0px; background-color: rgb(80,80,80); border-radius: 15px; z-index: 100;"
		},
		{
			kind: "enyo.Panels",
			fit: true,
			draggable: false,
			classes: "enyo-fill",
			name: "MainPanels",
			components: [
				{
					name: "ArticleListPanel",
					kind: "FittableRows",
					classes: "enyo-fill enyo-fit",
					components: [
						{
							kind: "onyx.Toolbar",
							style: "text-align: center;",
							components: [
								{
									style: "text-align: center; margin: auto; float: left;",
									components: [
										{
											style: "font-size: 14px;",
											name: "articleCount",
											content: "0"
										},
										{
											style: "font-size: 10px;",
											content: "Articles"
										}
									]
								},
								{content: "Mobo Reader"}
							]
						},
						{
							kind: "enyo.DataList",
							ontap: "articleSelected",
							name: "articleList",
							fit: true,
							scrollerOptions: {
								horizontal: "hidden",
								touch: true
							},
							fixedChildSize: LIST_ITEM_HEIGHT,
							components: [
								{kind: "moboreader.ArticleListItem" }
							]
						},
						{
							kind: "onyx.MoreToolbar",
							components: [
								{
									kind: "onyx.Button",
									content: "Refresh",
									ontap: "refreshTap",
									onhold: "refreshHold"
								},
								{
									kind: "onyx.Button",
									content: "Add",
									ontap: "showAddDialog"
								},
								{
									kind: "onyx.Button",
									content: "Settings",
									ontap: "settingsTap"
								}
							]
						}
					]
				},
				{
					name: "ArticleViewPanel",
					classes: "enyo-fill enyo-fit",
					onBack: "handleBackGesture",
					components: [
						{kind: "moboreader.ArticleView", name: "articleView", classes: "enyo-fill enyo-fit", onBack: "handleBackGesture"}
					]
				}
			]
		},
		{
			kind: "onyx.Popup",
			name: "refreshPopup",
			components: [
				{
					classes: "popup-buttons",
					kind: "onyx.Button",
					content: "Delete & refresh",
					ontap: "forceRefreshTap"
				},
				{
					classes: "popup-buttons",
					kind: "onyx.Button",
					content: "Full refresh",
					ontap: "refreshFullSyncTap"
				},
				{
					classes: "popup-buttons",
					kind: "onyx.Button",
					content: "Fast refresh",
					ontap: "refreshTap"
				}
			]
		},

		//non ui stuff:
		{
			name: "api",
			kind: "moboreader.Api"
		},
		{
			name: "addDialog",
			kind: "moboreader.AddDialog",
			onAdd: "addArticle"
		},
		{
			name: "settingsDialog",
			kind: "moboreader.SettingsDialog",
			onLogoutRequest: "logoutOfPocket"
		},
		{
			kind: "Signals",
			onAddArticle: "addArticle",
			onrelaunch: "addArticle",
			onArticleOpReturned: "continueWipe",
			onunload: "cleanUpOnUnload",

			onNeedShowAuth: "showAuth",
			onAuthOk: "refreshTap",
			onHideAuth: "hideAuth"
		}
	],
	create: function () {
		this.inherited(arguments);

		function fetchResult() {
			this.log("api collection fetch successful.");
			//do this after fetch to prevent empty list in UI.
			this.$.articleView.setCollection(this.articleCollection);
			this.$.articleList.set("collection", this.articleCollection);
			setTimeout(this.refreshTap.bind(this, null, null, false), 1000);

			if (window.PalmSystem) {
				window.PalmSystem.stageReady();
				if (window.PalmSystem.allowResizeOnPositiveSpaceChange) {
					window.PalmSystem.allowResizeOnPositiveSpaceChange(false); //deactivate keyboard resizing our app.
				}
				this.log("Launch Params: " + JSON.stringify(webos.launchParams()));
				if (webos.launchParams().url) {
					this.log("Adding article: " + webos.launchParams().url);
					this.$.api.addArticle(webos.launchParams().url, this.articleCollection);
				}
			}
		}

		this.articleCollection = new moboreader.ArticleCollection({url: "pocket-unread-list"});
		this.articleCollection.set("url", "pocket-unread-list");
		this.articleCollection.fetch({strategy: "merge", success: fetchResult.bind(this), fail: fetchResult.bind(this)});
		this.$.articleView.setApi(this.$.api);
	},
	cleanUpOnUnload: function () {
		this.log("Cleaning up on unload.");
		this.articleCollection.storeWithChilds();
	},

	showAddDialog: function () {
		this.$.addDialog.doShow();
		this.closeToolbarPopup();
	},
	addArticle: function (inSender, inEvent) {
		/*jslint unparam:true*/
		if (!inEvent || !inEvent.url) {
			this.warn("No url supplied. Can't add article.");
		}
		this.$.api.addArticle(inEvent.url, this.articleCollection, inEvent.title);
		//this.resize();
	},

	refreshTap: function (inSender, inEvent, slowSync) {
		/*jslint unparam:true*/
		if (this.refreshHeld) {
			this.refreshHeld = false;
		} else {
			this.$.api.downloadArticles(this.articleCollection, slowSync);
			this.$.refreshPopup.hide();
		}
		this.closeToolbarPopup();
	},
	refreshFullSyncTap: function () {
		this.refreshTap(null, null, true);
		this.$.refreshPopup.hide();
	},
	refreshHold: function (inSender, inEvent) {
		this.refreshHeld = true;
		//show popup with more refresh options.
		this.$.refreshPopup.showAtEvent(inEvent);
	},
	forceRefreshTap: function () {
		this.articleCollection.whipe();
		this.dbOpId = ArticleContentHandler.wipe();
		this.$.refreshPopup.hide();
	},
	continueWipe: function (inSender, inEvent) {
		/*jslint unparam:true*/
		if (inEvent.activityId === this.dbOpId) {
			if (this.loggingOut) {
				this.$.api.logout();
				this.loggingOut = false;
			} else {
				this.$.api.downloadArticles(this.articleCollection, true);
			}
		}
	},
	settingsTap: function () {
		this.$.settingsDialog.show();
		this.closeToolbarPopup();
	},
	logoutOfPocket: function () {
		this.loggingOut = true;
		this.forceRefreshTap();
	},

	articleSelected: function (inSender, inEvent) {
		/*jslint unparam:true*/
		this.lastArticle = this.$.articleList.selected();
		if (this.lastArticle) {
			this.$.articleView.setArticleModel(this.lastArticle);
			this.$.MainPanels.setIndex(1);
			this.$.articleList.deselectAll();
		}
	},
	handleBackGesture: function () {
		if (this.$.MainPanels.getIndex() === 2) {
			this.log("In auth dialog, prevent back!");
			return;
		}

		this.$.MainPanels.setIndex(0);
		this.$.settingsDialog.hide();

		//this only works, because I hacked enyo/source/ui/data/VerticalDelegate.js scrollToIndex to always scroll to top, i.e. replaced line 291 wiht list.$.scroller.scrollIntoView(c, true);
		if (this.lastArticle) {
			var scrollTo = this.articleCollection.indexOf(this.lastArticle),
				listHeight = this.$.articleList.getBounds().height,
				numArticlesOnScreen = listHeight / LIST_ITEM_HEIGHT;

			scrollTo -= Math.round((numArticlesOnScreen - 1) / 2); //list will scroll to top, allow some articles above it to show up, too.

			if (scrollTo >= this.articleCollection.length) {
				scrollTo = this.articleCollection.length - 1;
			} else if (scrollTo < 0) {
				scrollTo = 0;
			}
			this.$.articleList.scrollToIndex(scrollTo);
		}
	},

	showAuth: function (inSender, inEvent) {
		if (!this.authCreated) {
			this.authCreated = true;
			var authDialog = this.$.MainPanels.createComponent({
				name: "AuthPanel",
				kind: "moboreader.AuthDialog"
			});
			this.$.MainPanels.render();
			//need to hand signal for auth dialog to catch it:
			authDialog.prepareAuthDialog(inSender, inEvent);
		}
		this.lastPanelIndex = this.$.MainPanels.getIndex();
		this.$.MainPanels.setIndex(2);
	},
	hideAuth: function () {
		if (this.$.MainPanels.getIndex() === 2) {
			this.$.MainPanels.setIndex(this.lastPanelIndex || 0);
		}
	},
	closeToolbarPopup: function () {
		if (this && this.$ && this.$.moreToolbar && this.$.moreToolbar.$ && this.$.moreToolbar.$.menu) {
			this.$.moreToolbar.$.menu.hide();
		}
	}
});
<|endoftext|>


<commit_before>'use strict';

const titleContainer = '[class*=PlayerControlsMetadata]';

const plexFilter = MetadataFilter.createFilter({
	album: removeUnknownAlbum,
});

Connector.playerSelector = '[class^=AudioVideoPlayerView-container]';

Connector.trackSelector = `${titleContainer} a[class*=MetadataPosterTitle]`;

Connector.albumSelector = `${titleContainer} [class*=MetadataPosterTitle-title] > a:nth-child(3)`;

Connector.artistSelector = `${titleContainer} [class*=MetadataPosterTitle-title] > a:nth-child(1)`;

Connector.timeInfoSelector = `${Connector.playerSelector} [class*=DurationRemaining-container]`;

// @ifndef FIREFOX
// NOTE: Blob URLs used in Plex aren't supported by Firefox 67 and older.
Connector.trackArtSelector = [
	`${Connector.playerSelector} [class^=PosterCardImg-imageContainer] div`,
	'[class^=AudioVideoFullPlayer] [class^=PosterCardImg-imageContainer] div',
];
// @endif

Connector.pauseButtonSelector = [
	`${Connector.playerSelector} [data-qa-id="pauseButton"]`,
	`${Connector.playerSelector} [class^=plex-icon-player-pause]`,
];

// For watch-it-later videos
Connector.artistTrackSelector = `${Connector.playerSelector} [class*=MetadataPosterTitle-title]`;

Connector.applyFilter(MetadataFilter.getYoutubeFilter().extend(plexFilter));

Connector.getTrack = () => {
	if (Connector.getArtist()) {
		return $(Connector.trackSelector).text();
	}
	return null;
};

function removeUnknownAlbum(text) {
	return text.replace('[Unknown Album]', '');
}
<commit_msg>Fix plex connector<commit_after>'use strict';

const titleContainer = '[class*=PlayerControlsMetadata]';

const plexFilter = MetadataFilter.createFilter({
	album: removeUnknownAlbum,
});

Connector.playerSelector = '[class^=Player-miniPlayerContainer]';

Connector.trackSelector = `${titleContainer} a[class*=MetadataPosterTitle]`;

Connector.albumSelector = `${titleContainer} [class*=MetadataPosterTitle-title] > a:nth-child(3)`;

Connector.artistSelector = `${titleContainer} [class*=MetadataPosterTitle-title] > a:nth-child(1)`;

Connector.timeInfoSelector = `${Connector.playerSelector} [class*=DurationRemaining-container]`;

// @ifndef FIREFOX
// NOTE: Blob URLs used in Plex aren't supported by Firefox 67 and older.
Connector.trackArtSelector = [
	`${Connector.playerSelector} [class^=PosterCardImg-imageContainer] div`,
	'[class^=AudioVideoFullPlayer] [class^=PosterCardImg-imageContainer] div',
];
// @endif

Connector.pauseButtonSelector = [
	`${Connector.playerSelector} [data-qa-id="pauseButton"]`,
	`${Connector.playerSelector} [class^=plex-icon-player-pause]`,
];

// For watch-it-later videos
Connector.artistTrackSelector = `${Connector.playerSelector} [class*=MetadataPosterTitle-title]`;

Connector.applyFilter(MetadataFilter.getYoutubeFilter().extend(plexFilter));

Connector.getTrack = () => {
	if (Connector.getArtist()) {
		return $(Connector.trackSelector).text();
	}
	return null;
};

function removeUnknownAlbum(text) {
	return text.replace('[Unknown Album]', '');
}
<|endoftext|>


<commit_before>Logoish = (function() {
  "use strict";

  var self = {};

  var defaults = {
    lineWidth: 3,
    lineColor: "black"
  };

  var state = {
    lineWidth: defaults.lineWidth,
    lineColor: defaults.lineColor,
    x: 0,
    y: 0,
    angle: 0,
    speed: 5
  }

  var queue = [];

  var context = null;

  var animate = {};

  self.init = function() {
    createCanvas();
    setToplevel();
    _setState();
    _positionTurtle();
    _stepTurtle();
  }

  self.clear = function() {
    queue.push( ["clear"]);
  }

  animate.clear = function() {
    context.clearRect(0,0,state.width,state.height);
  }

  self.reset = function() {
    queue.push( ["reset"]);
  }

  animate.reset = function() {
    animate.clear();
    state.lineWidth = defaults.lineWidth;
    state.lineColor = defaults.lineColor;
    state.x = Math.floor(state.width / 2);
    state.y = Math.floor(state.height / 2);
    state.angle = 0;
    animate.show();
    _positionTurtle();
  }

  self.lineWidth = function(width) {
    queue.push( ["lineWidth", width ]);
  }

  animate.lineWidth = function(entry) {
    state.lineWidth = entry[1];
    _setState();
  }

  self.lineColor = function(color) {
    queue.push( ["lineColor", color]);
  }

  animate.lineColor = function(entry) {
    state.lineColor = entry[1];
    _setState();
  };

  self.forward = function(distance) {
    queue.push([ "forward", distance ]);
  }

  animate.forward = function(entry) {
    if(!entry[2]) {
      entry[2] = state.x;
      entry[3] = state.y;
    }
    var distance = entry[1];
    if(state.speed && distance > state.speed) {
      distance = state.speed;
      entry[1] -= state.speed;
      queue.unshift(entry);
    }
    var dx = Math.sin(Math.PI / 180 * state.angle) * distance;
    var dy = -Math.cos(Math.PI / 180 * state.angle) * distance;

    context.beginPath();
    context.moveTo(entry[2],entry[3]);
    state.x += dx;
    state.y += dy;
    context.lineTo(state.x,state.y);
    context.stroke();
    _positionTurtle();
  }

  self.arcRight = function(radius, angle) {
    queue.push([ "arc", radius, angle, false ]);
  }

  self.arcLeft = function(radius, angle) {
    queue.push([ "arc", radius, angle, true]);
  }

  animate.arc = function(entry) {
    var radius = entry[0];
    var angle = entry[1];

    var calc = entry[4];

    if(!entry[4]) {
      calc = entry[4] = {};

      calc.x = state.x + -radius * Math.sin(Math.PI / 180 * state.angle);
      calc.y = state.y + r-adius * Math.cos(Math.PI / 180 * state.angle);

      calc.start = state.angle;
      calc.end = calc.start + entry[4];
    }

    // get the starting angle from our angle

    // find the center

    // calc the start and end angle once

    // draw as much of the arc as we like
  }

  self.skip = function(distance) {
    queue.push([ "skip", distance ]);
  }

  animate.skip = function(entry) {
    var distance = entry[1];

    var dx = Math.sin(Math.PI / 180 * state.angle) * distance;
    var dy = -Math.cos(Math.PI / 180 * state.angle) * distance;

    state.x += dx;
    state.y += dy;
    _positionTurtle();
  }

  self.rotate = function(angle) {
    queue.push([ "rotate", angle ]);
  }

  animate.rotate = function(entry) {
    var angle = entry[1];
    if(state.speed && Math.abs(angle) > state.speed) {
      angle = angle > 0 ? state.speed : -state.speed;
      entry[1] -= angle;
      queue.unshift(entry);
    } else {
      state.angle = state.angle % 360;

    }

    state.angle += angle;
    _positionTurtle();
  };

  self.moveTo = function(x,y) {
    queue.push(["moveTo", x, y ]);
  };

  animate.moveTo = function(entry) {
    state.x = entry[1];
    state.y = entry[2];
    _positionTurtle();
  }

  self.move = function(x,y) {
    queue.push(["move", x, y ]);
  };

  animate.move = function(entry) {
    state.x += entry[1];
    state.y += entry[2];
    _positionTurtle();
  }

  self.hide = function() {
    queue.push(["hide"]);
  };

  animate.hide = function() {
    turtle.style.display = "none";
  };

  self.show = function() {
    queue.push(["show"]);
  };

  animate.show = function() {
    turtle.style.display = "block";
  };

  self.angle = function(angle) {
    queue.push(["angle", angle]);
  };

  animate.angle = function(entry) {
    state.angle = entry[1];
  };

  self.speed = function(speed) {
    queue.push(["speed", speed]);
  };

  animate.speed = function(entry) {
    state.speed = entry[1];
  };

  self.stop = function() {
    queue = [];
  };

  function _setState() {
    context.strokeStyle =  state.lineColor;
    context.lineWidth = state.lineWidth;
  }

  function _positionTurtle() {
    self.turtle.style.left = (state.x-5) + "px";
    self.turtle.style.top = (state.y-10)  + "px";

    self.turtle.style.transform = "rotate(" + state.angle + "deg)";
  }

  function _stepTurtle(n) {
    if(queue.length > 0) {
      var current = queue.shift();
      animate[current[0]](current);
    }
    if(state.speed == 0 && queue.length > 0) {
      if(n < 100) {
        _stepTurtle(n ? (n+1) : 1);
      }
    }
    requestAnimationFrame(_stepTurtle);
  }

  function createCanvas() {
    var body = document.getElementsByTagName("body")[0];
    var div = document.createElement("div")
    div.style.position = "absolute";
    div.style.width = "100%";
    div.style.height = "100%";

    body.appendChild(div);

    self.canvas = document.createElement("canvas");
    state.width = self.canvas.width = div.offsetWidth;
    state.height = self.canvas.height = div.offsetHeight;

    div.appendChild(self.canvas);
    context = self.canvas.getContext("2d");

    var turtle = self.turtle = document.createElement("div");
    turtle.style.position = "absolute";
    turtle.style.width = 0;
    turtle.style.height= 0;
    turtle.style.borderLeft = "5px solid transparent";
    turtle.style.borderRight= "5px solid transparent";
    turtle.style.borderBottom = "20px solid black";
    div.appendChild(turtle);

    self.reset();
  }

  function setToplevel() {
    for(var name in self) {
      if(name[0] != "_" && name != "init") {
        window[name] = self[name];
      }
    }
  }

  return self;
})();
<commit_msg>Speed<commit_after>Logoish = (function() {
  "use strict";

  var self = {};

  var defaults = {
    lineWidth: 3,
    lineColor: "black"
  };

  var state = {
    lineWidth: defaults.lineWidth,
    lineColor: defaults.lineColor,
    x: 0,
    y: 0,
    angle: 0,
    speed: 5
  }

  var queue = [];

  var context = null;

  var animate = {};

  self.init = function() {
    createCanvas();
    setToplevel();
    _setState();
    _positionTurtle();
    _stepTurtle();
  }

  self.clear = function() {
    queue.push( ["clear"]);
  }

  animate.clear = function() {
    context.clearRect(0,0,state.width,state.height);
  }

  self.reset = function() {
    queue.push( ["reset"]);
  }

  animate.reset = function() {
    animate.clear();
    state.lineWidth = defaults.lineWidth;
    state.lineColor = defaults.lineColor;
    state.x = Math.floor(state.width / 2);
    state.y = Math.floor(state.height / 2);
    state.angle = 0;
    animate.show();
    _positionTurtle();
  }

  self.lineWidth = function(width) {
    queue.push( ["lineWidth", width ]);
  }

  animate.lineWidth = function(entry) {
    state.lineWidth = entry[1];
    _setState();
  }

  self.lineColor = function(color) {
    queue.push( ["lineColor", color]);
  }

  animate.lineColor = function(entry) {
    state.lineColor = entry[1];
    _setState();
  };

  self.forward = function(distance) {
    queue.push([ "forward", distance ]);
  }

  animate.forward = function(entry) {
    if(!entry[2]) {
      entry[2] = state.x;
      entry[3] = state.y;
    }
    var distance = entry[1];
    if(state.speed && distance > state.speed) {
      distance = state.speed;
      entry[1] -= state.speed;
      queue.unshift(entry);
    }
    var dx = Math.sin(Math.PI / 180 * state.angle) * distance;
    var dy = -Math.cos(Math.PI / 180 * state.angle) * distance;

    context.beginPath();
    context.moveTo(entry[2],entry[3]);
    state.x += dx;
    state.y += dy;
    context.lineTo(state.x,state.y);
    context.stroke();
    _positionTurtle();
  }

  self.arcRight = function(radius, angle) {
    queue.push([ "arc", radius, angle, false ]);
  }

  self.arcLeft = function(radius, angle) {
    queue.push([ "arc", radius, angle, true]);
  }

  animate.arc = function(entry) {
    var radius = entry[0];
    var angle = entry[1];

    var calc = entry[4];

    if(!entry[4]) {
      calc = entry[4] = {};

      calc.x = state.x + -radius * Math.sin(Math.PI / 180 * state.angle);
      calc.y = state.y + r-adius * Math.cos(Math.PI / 180 * state.angle);

      calc.start = state.angle;
      calc.end = calc.start + entry[4];
    }

    // get the starting angle from our angle

    // find the center

    // calc the start and end angle once

    // draw as much of the arc as we like
  }

  self.skip = function(distance) {
    queue.push([ "skip", distance ]);
  }

  animate.skip = function(entry) {
    var distance = entry[1];

    var dx = Math.sin(Math.PI / 180 * state.angle) * distance;
    var dy = -Math.cos(Math.PI / 180 * state.angle) * distance;

    state.x += dx;
    state.y += dy;
    _positionTurtle();
  }

  self.rotate = function(angle) {
    queue.push([ "rotate", angle ]);
  }

  animate.rotate = function(entry) {
    var angle = entry[1];
    if(state.speed && Math.abs(angle) > state.speed) {
      angle = angle > 0 ? state.speed : -state.speed;
      entry[1] -= angle;
      queue.unshift(entry);
    } else {
      state.angle = state.angle % 360;

    }

    state.angle += angle;
    _positionTurtle();
  };

  self.moveTo = function(x,y) {
    queue.push(["moveTo", x, y ]);
  };

  animate.moveTo = function(entry) {
    state.x = entry[1];
    state.y = entry[2];
    _positionTurtle();
  }

  self.move = function(x,y) {
    queue.push(["move", x, y ]);
  };

  animate.move = function(entry) {
    state.x += entry[1];
    state.y += entry[2];
    _positionTurtle();
  }

  self.hide = function() {
    queue.push(["hide"]);
  };

  animate.hide = function() {
    turtle.style.display = "none";
  };

  self.show = function() {
    queue.push(["show"]);
  };

  animate.show = function() {
    turtle.style.display = "block";
  };

  self.angle = function(angle) {
    queue.push(["angle", angle]);
  };

  animate.angle = function(entry) {
    state.angle = entry[1];
  };

  self.speed = function(speed) {
    queue.push(["speed", speed]);
  };

  animate.speed = function(entry) {
    state.speed = entry[1];
  };

  self.stop = function() {
    queue = [];
  };

  function _setState() {
    context.strokeStyle =  state.lineColor;
    context.lineWidth = state.lineWidth;
  }

  function _positionTurtle() {
    self.turtle.style.left = (state.x-5) + "px";
    self.turtle.style.top = (state.y-10)  + "px";

    self.turtle.style.transform = "rotate(" + state.angle + "deg)";
  }

  function _stepTurtle(n) {
    if(queue.length > 0) {
      var current = queue.shift();
      animate[current[0]](current);
    }
    if(state.speed == 0 && queue.length > 0) {
      if(n < 100) {
        _stepTurtle(n ? (n+1) : 1);
      }
    }
    if(!n) {
      requestAnimationFrame(_stepTurtle);
    }
  }

  function createCanvas() {
    var body = document.getElementsByTagName("body")[0];
    var div = document.createElement("div")
    div.style.position = "absolute";
    div.style.width = "100%";
    div.style.height = "100%";

    body.appendChild(div);

    self.canvas = document.createElement("canvas");
    state.width = self.canvas.width = div.offsetWidth;
    state.height = self.canvas.height = div.offsetHeight;

    div.appendChild(self.canvas);
    context = self.canvas.getContext("2d");

    var turtle = self.turtle = document.createElement("div");
    turtle.style.position = "absolute";
    turtle.style.width = 0;
    turtle.style.height= 0;
    turtle.style.borderLeft = "5px solid transparent";
    turtle.style.borderRight= "5px solid transparent";
    turtle.style.borderBottom = "20px solid black";
    div.appendChild(turtle);

    self.reset();
  }

  function setToplevel() {
    for(var name in self) {
      if(name[0] != "_" && name != "init") {
        window[name] = self[name];
      }
    }
  }

  return self;
})();
<|endoftext|>


<commit_before>const createRetry = require('../retry')
const limitConcurrency = require('../utils/concurrency')
const { KafkaJSError } = require('../errors')
const {
  events: { GROUP_JOIN, FETCH, FETCH_START, START_BATCH_PROCESS, END_BATCH_PROCESS },
} = require('./instrumentationEvents')

const isTestMode = process.env.NODE_ENV === 'test'

const isRebalancing = e =>
  e.type === 'REBALANCE_IN_PROGRESS' || e.type === 'NOT_COORDINATOR_FOR_GROUP'

const isKafkaJSError = e => e instanceof KafkaJSError

module.exports = class Runner {
  constructor({
    logger,
    consumerGroup,
    instrumentationEmitter,
    eachBatchAutoResolve = true,
    partitionsConsumedConcurrently,
    eachBatch,
    eachMessage,
    heartbeatInterval,
    onCrash,
    retry,
    autoCommit = true,
  }) {
    this.logger = logger.namespace('Runner')
    this.consumerGroup = consumerGroup
    this.instrumentationEmitter = instrumentationEmitter
    this.eachBatchAutoResolve = eachBatchAutoResolve
    this.eachBatch = eachBatch
    this.eachMessage = eachMessage
    this.heartbeatInterval = heartbeatInterval
    this.retrier = createRetry(Object.assign({}, retry))
    this.onCrash = onCrash
    this.autoCommit = autoCommit
    this.partitionsConsumedConcurrently = partitionsConsumedConcurrently

    this.running = false
    this.consuming = false
  }

  async join() {
    const startJoin = Date.now()
    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.join()
        await this.consumerGroup.sync()

        this.running = true

        const memberAssignment = this.consumerGroup
          .assigned()
          .reduce((result, { topic, partitions }) => ({ ...result, [topic]: partitions }), {})

        const payload = {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          leaderId: this.consumerGroup.leaderId,
          isLeader: this.consumerGroup.isLeader(),
          memberAssignment,
          groupProtocol: this.consumerGroup.groupProtocol,
          duration: Date.now() - startJoin,
        }

        this.instrumentationEmitter.emit(GROUP_JOIN, payload)
        this.logger.info('Consumer has joined the group', payload)
      } catch (e) {
        if (isRebalancing(e)) {
          // Rebalance in progress isn't a retriable error since the consumer
          // has to go through find coordinator and join again before it can
          // actually retry. Throwing a retriable error to allow the retrier
          // to keep going
          throw new KafkaJSError('The group is rebalancing')
        }

        bail(e)
      }
    })
  }

  async scheduleJoin() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.join().catch(this.onCrash)
  }

  async start() {
    if (this.running) {
      return
    }

    try {
      await this.consumerGroup.connect()
      await this.join()

      this.running = true
      this.scheduleFetch()
    } catch (e) {
      this.onCrash(e)
    }
  }

  async stop() {
    if (!this.running) {
      return
    }

    this.logger.debug('stop consumer group', {
      groupId: this.consumerGroup.groupId,
      memberId: this.consumerGroup.memberId,
    })

    this.running = false

    try {
      if (!isTestMode) {
        await this.waitForConsumer()
      }
      await this.consumerGroup.leave()
    } catch (e) {}
  }

  waitForConsumer() {
    return new Promise(resolve => {
      const scheduleWait = () => {
        this.logger.debug('waiting for consumer to finish...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
        })

        setTimeout(() => (!this.consuming ? resolve() : scheduleWait()), 1000)
      }

      if (!this.consuming) {
        return resolve()
      }

      scheduleWait()
    })
  }

  async processEachMessage(batch) {
    const { topic, partition } = batch

    for (const message of batch.messages) {
      if (!this.running || this.consumerGroup.hasSeekOffset({ topic, partition })) {
        break
      }

      try {
        await this.eachMessage({ topic, partition, message })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
          })
        }

        // In case of errors, commit the previously consumed offsets
        await this.consumerGroup.commitOffsets()
        throw e
      }

      this.consumerGroup.resolveOffset({ topic, partition, offset: message.offset })
      await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
      await this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  async processEachBatch(batch) {
    const { topic, partition } = batch

    try {
      await this.eachBatch({
        batch,
        resolveOffset: offset => {
          this.consumerGroup.resolveOffset({ topic, partition, offset })
        },
        heartbeat: async () => {
          await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
        },
        /**
         * Commit offsets if provided. Otherwise commit most recent resolved offsets
         * if the autoCommit conditions are met.
         *
         * @param {OffsetsByTopicPartition} [offsets] Optional.
         */
        commitOffsetsIfNecessary: async offsets => {
          return offsets
            ? this.consumerGroup.commitOffsets(offsets)
            : this.consumerGroup.commitOffsetsIfNecessary()
        },
        uncommittedOffsets: () => this.consumerGroup.uncommittedOffsets(),
        isRunning: () => this.running,
        isStale: () => this.consumerGroup.hasSeekOffset({ topic, partition }),
      })
    } catch (e) {
      if (!isKafkaJSError(e)) {
        this.logger.error(`Error when calling eachBatch`, {
          topic,
          partition,
          offset: batch.firstOffset(),
          stack: e.stack,
        })
      }

      // eachBatch has a special resolveOffset which can be used
      // to keep track of the messages
      await this.autoCommitOffsets()
      throw e
    }

    // resolveOffset for the last offset can be disabled to allow the users of eachBatch to
    // stop their consumers without resolving unprocessed offsets (issues/18)
    if (this.eachBatchAutoResolve) {
      this.consumerGroup.resolveOffset({ topic, partition, offset: batch.lastOffset() })
    }
  }

  async fetch() {
    const startFetch = Date.now()

    this.instrumentationEmitter.emit(FETCH_START, {})

    const batches = await this.consumerGroup.fetch()

    this.instrumentationEmitter.emit(FETCH, {
      numberOfBatches: batches.length,
      duration: Date.now() - startFetch,
    })

    const onBatch = async batch => {
      const startBatchProcess = Date.now()
      const payload = {
        topic: batch.topic,
        partition: batch.partition,
        highWatermark: batch.highWatermark,
        offsetLag: batch.offsetLag(),
        /**
         * @since 2019-06-24 (>= 1.8.0)
         *
         * offsetLag returns the lag based on the latest offset in the batch, to
         * keep the event backward compatible we just introduced "offsetLagLow"
         * which calculates the lag based on the first offset in the batch
         */
        offsetLagLow: batch.offsetLagLow(),
        batchSize: batch.messages.length,
        firstOffset: batch.firstOffset(),
        lastOffset: batch.lastOffset(),
      }

      this.instrumentationEmitter.emit(START_BATCH_PROCESS, payload)

      if (this.eachMessage) {
        await this.processEachMessage(batch)
      } else if (this.eachBatch) {
        await this.processEachBatch(batch)
      }

      this.instrumentationEmitter.emit(END_BATCH_PROCESS, {
        ...payload,
        duration: Date.now() - startBatchProcess,
      })
    }

    const concurrently = limitConcurrency({ limit: this.partitionsConsumedConcurrently })
    await Promise.all(
      batches.map(batch =>
        concurrently(async () => {
          if (!this.running) {
            return
          }

          if (batch.isEmpty()) {
            return
          }

          await onBatch(batch)
        })
      )
    )

    await this.autoCommitOffsets()
    await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
  }

  async scheduleFetch() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        this.consuming = true
        await this.fetch()
        this.consuming = false
        setImmediate(() => this.scheduleFetch())
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          await this.join()
          this.scheduleFetch()
          return
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          await this.join()
          this.scheduleFetch()
          return
        }

        if (e.name === 'KafkaJSOffsetOutOfRange') {
          this.scheduleFetch()
          return
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while fetching data, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
        })

        throw e
      } finally {
        this.consuming = false
      }
    }).catch(this.onCrash)
  }

  autoCommitOffsets() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsets()
    }
  }

  autoCommitOffsetsIfNecessary() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  commitOffsets(offsets) {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
        offsets,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.commitOffsets(offsets)
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            offsets,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while committing offsets, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
          offsets,
        })

        throw e
      }
    })
  }
}
<commit_msg>#751 all recursive calls to scheduleFetch scheduled with setImmedate()<commit_after>const createRetry = require('../retry')
const limitConcurrency = require('../utils/concurrency')
const { KafkaJSError } = require('../errors')
const {
  events: { GROUP_JOIN, FETCH, FETCH_START, START_BATCH_PROCESS, END_BATCH_PROCESS },
} = require('./instrumentationEvents')

const isTestMode = process.env.NODE_ENV === 'test'

const isRebalancing = e =>
  e.type === 'REBALANCE_IN_PROGRESS' || e.type === 'NOT_COORDINATOR_FOR_GROUP'

const isKafkaJSError = e => e instanceof KafkaJSError

module.exports = class Runner {
  constructor({
    logger,
    consumerGroup,
    instrumentationEmitter,
    eachBatchAutoResolve = true,
    partitionsConsumedConcurrently,
    eachBatch,
    eachMessage,
    heartbeatInterval,
    onCrash,
    retry,
    autoCommit = true,
  }) {
    this.logger = logger.namespace('Runner')
    this.consumerGroup = consumerGroup
    this.instrumentationEmitter = instrumentationEmitter
    this.eachBatchAutoResolve = eachBatchAutoResolve
    this.eachBatch = eachBatch
    this.eachMessage = eachMessage
    this.heartbeatInterval = heartbeatInterval
    this.retrier = createRetry(Object.assign({}, retry))
    this.onCrash = onCrash
    this.autoCommit = autoCommit
    this.partitionsConsumedConcurrently = partitionsConsumedConcurrently

    this.running = false
    this.consuming = false
  }

  async join() {
    const startJoin = Date.now()
    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.join()
        await this.consumerGroup.sync()

        this.running = true

        const memberAssignment = this.consumerGroup
          .assigned()
          .reduce((result, { topic, partitions }) => ({ ...result, [topic]: partitions }), {})

        const payload = {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          leaderId: this.consumerGroup.leaderId,
          isLeader: this.consumerGroup.isLeader(),
          memberAssignment,
          groupProtocol: this.consumerGroup.groupProtocol,
          duration: Date.now() - startJoin,
        }

        this.instrumentationEmitter.emit(GROUP_JOIN, payload)
        this.logger.info('Consumer has joined the group', payload)
      } catch (e) {
        if (isRebalancing(e)) {
          // Rebalance in progress isn't a retriable error since the consumer
          // has to go through find coordinator and join again before it can
          // actually retry. Throwing a retriable error to allow the retrier
          // to keep going
          throw new KafkaJSError('The group is rebalancing')
        }

        bail(e)
      }
    })
  }

  async scheduleJoin() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.join().catch(this.onCrash)
  }

  async start() {
    if (this.running) {
      return
    }

    try {
      await this.consumerGroup.connect()
      await this.join()

      this.running = true
      this.scheduleFetch()
    } catch (e) {
      this.onCrash(e)
    }
  }

  async stop() {
    if (!this.running) {
      return
    }

    this.logger.debug('stop consumer group', {
      groupId: this.consumerGroup.groupId,
      memberId: this.consumerGroup.memberId,
    })

    this.running = false

    try {
      if (!isTestMode) {
        await this.waitForConsumer()
      }
      await this.consumerGroup.leave()
    } catch (e) {}
  }

  waitForConsumer() {
    return new Promise(resolve => {
      const scheduleWait = () => {
        this.logger.debug('waiting for consumer to finish...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
        })

        setTimeout(() => (!this.consuming ? resolve() : scheduleWait()), 1000)
      }

      if (!this.consuming) {
        return resolve()
      }

      scheduleWait()
    })
  }

  async processEachMessage(batch) {
    const { topic, partition } = batch

    for (const message of batch.messages) {
      if (!this.running || this.consumerGroup.hasSeekOffset({ topic, partition })) {
        break
      }

      try {
        await this.eachMessage({ topic, partition, message })
      } catch (e) {
        if (!isKafkaJSError(e)) {
          this.logger.error(`Error when calling eachMessage`, {
            topic,
            partition,
            offset: message.offset,
            stack: e.stack,
          })
        }

        // In case of errors, commit the previously consumed offsets
        await this.consumerGroup.commitOffsets()
        throw e
      }

      this.consumerGroup.resolveOffset({ topic, partition, offset: message.offset })
      await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
      await this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  async processEachBatch(batch) {
    const { topic, partition } = batch

    try {
      await this.eachBatch({
        batch,
        resolveOffset: offset => {
          this.consumerGroup.resolveOffset({ topic, partition, offset })
        },
        heartbeat: async () => {
          await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
        },
        /**
         * Commit offsets if provided. Otherwise commit most recent resolved offsets
         * if the autoCommit conditions are met.
         *
         * @param {OffsetsByTopicPartition} [offsets] Optional.
         */
        commitOffsetsIfNecessary: async offsets => {
          return offsets
            ? this.consumerGroup.commitOffsets(offsets)
            : this.consumerGroup.commitOffsetsIfNecessary()
        },
        uncommittedOffsets: () => this.consumerGroup.uncommittedOffsets(),
        isRunning: () => this.running,
        isStale: () => this.consumerGroup.hasSeekOffset({ topic, partition }),
      })
    } catch (e) {
      if (!isKafkaJSError(e)) {
        this.logger.error(`Error when calling eachBatch`, {
          topic,
          partition,
          offset: batch.firstOffset(),
          stack: e.stack,
        })
      }

      // eachBatch has a special resolveOffset which can be used
      // to keep track of the messages
      await this.autoCommitOffsets()
      throw e
    }

    // resolveOffset for the last offset can be disabled to allow the users of eachBatch to
    // stop their consumers without resolving unprocessed offsets (issues/18)
    if (this.eachBatchAutoResolve) {
      this.consumerGroup.resolveOffset({ topic, partition, offset: batch.lastOffset() })
    }
  }

  async fetch() {
    const startFetch = Date.now()

    this.instrumentationEmitter.emit(FETCH_START, {})

    const batches = await this.consumerGroup.fetch()

    this.instrumentationEmitter.emit(FETCH, {
      numberOfBatches: batches.length,
      duration: Date.now() - startFetch,
    })

    const onBatch = async batch => {
      const startBatchProcess = Date.now()
      const payload = {
        topic: batch.topic,
        partition: batch.partition,
        highWatermark: batch.highWatermark,
        offsetLag: batch.offsetLag(),
        /**
         * @since 2019-06-24 (>= 1.8.0)
         *
         * offsetLag returns the lag based on the latest offset in the batch, to
         * keep the event backward compatible we just introduced "offsetLagLow"
         * which calculates the lag based on the first offset in the batch
         */
        offsetLagLow: batch.offsetLagLow(),
        batchSize: batch.messages.length,
        firstOffset: batch.firstOffset(),
        lastOffset: batch.lastOffset(),
      }

      this.instrumentationEmitter.emit(START_BATCH_PROCESS, payload)

      if (this.eachMessage) {
        await this.processEachMessage(batch)
      } else if (this.eachBatch) {
        await this.processEachBatch(batch)
      }

      this.instrumentationEmitter.emit(END_BATCH_PROCESS, {
        ...payload,
        duration: Date.now() - startBatchProcess,
      })
    }

    const concurrently = limitConcurrency({ limit: this.partitionsConsumedConcurrently })
    await Promise.all(
      batches.map(batch =>
        concurrently(async () => {
          if (!this.running) {
            return
          }

          if (batch.isEmpty()) {
            return
          }

          await onBatch(batch)
        })
      )
    )

    await this.autoCommitOffsets()
    await this.consumerGroup.heartbeat({ interval: this.heartbeatInterval })
  }

  async scheduleFetch() {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        this.consuming = true
        await this.fetch()
        this.consuming = false
        setImmediate(() => this.scheduleFetch())
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          await this.join()
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          await this.join()
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.name === 'KafkaJSOffsetOutOfRange') {
          setImmediate(() => this.scheduleFetch())
          return
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while fetching data, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
        })

        throw e
      } finally {
        this.consuming = false
      }
    }).catch(this.onCrash)
  }

  autoCommitOffsets() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsets()
    }
  }

  autoCommitOffsetsIfNecessary() {
    if (this.autoCommit) {
      return this.consumerGroup.commitOffsetsIfNecessary()
    }
  }

  commitOffsets(offsets) {
    if (!this.running) {
      this.logger.debug('consumer not running, exiting', {
        groupId: this.consumerGroup.groupId,
        memberId: this.consumerGroup.memberId,
        offsets,
      })
      return
    }

    return this.retrier(async (bail, retryCount, retryTime) => {
      try {
        await this.consumerGroup.commitOffsets(offsets)
      } catch (e) {
        if (!this.running) {
          this.logger.debug('consumer not running, exiting', {
            error: e.message,
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            offsets,
          })
          return
        }

        if (isRebalancing(e)) {
          this.logger.error('The group is rebalancing, re-joining', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.type === 'UNKNOWN_MEMBER_ID') {
          this.logger.error('The coordinator is not aware of this member, re-joining the group', {
            groupId: this.consumerGroup.groupId,
            memberId: this.consumerGroup.memberId,
            error: e.message,
            retryCount,
            retryTime,
          })

          this.consumerGroup.memberId = null
          setImmediate(() => this.scheduleJoin())

          bail(new KafkaJSError('The group is rebalancing'))
        }

        if (e.name === 'KafkaJSNotImplemented') {
          return bail(e)
        }

        this.logger.debug('Error while committing offsets, trying again...', {
          groupId: this.consumerGroup.groupId,
          memberId: this.consumerGroup.memberId,
          error: e.message,
          stack: e.stack,
          retryCount,
          retryTime,
          offsets,
        })

        throw e
      }
    })
  }
}
<|endoftext|>