I have an API that send updates via Server Sent Events (SSE) every seconds for my items.
Basically I have a collection $scope.items that contain a lot of information within and every second one item of this list is updated.
What I'm doing is:
var source;
if (!!window.EventSource) {
source = new EventSource('/updates');
} else {
alertify.error('SSE not supported');
}
// Emit SSE for items
source.addEventListener('items', function (e) {
var data = JSON.parse(e.data);
$timeout(function () {
var item_index = _.findIndex($scope.items, function (item) {
return item.id === data.id;
});
var status = data.status;
if (item_index > -1) {
if (status === 'cancelled') {
$scope.items.splice(item_index, 1);
}
$scope.items[item_index] = data;
$scope.$apply();
} else {
$scope.items.push(data);
}
});
}, false);
I was wondering if I'm doing it right or if I can improve this code because the app is quite slow when I start to have many and many items to cycle every second...
Looking at your code:
var item_index = _.findIndex($scope.items, function (item) {
return item.id === data.id;
});
I fear that a full search is done every time you access item_index
I would define a function:
function getIndex(data){
_.findIndex($scope.items, function (item) {
return item.id === data.id;
});
};
And the call it from within your
$timeout(function () {
var item_index = getIndex(data);
...
Related
I am building a chrome extension and getting datas from a webpage(a list with 20 elements and hundreds of pages ) through my injected script.
This injected script is sending the datas via chrome storage to the background script.
The background script is calculating an array.
Then sending the result to my content script where a mutation observer is waiting for an event where it’s using the calculated array.
I am sending all these data’s around by chrome.local.storage.set / get.
Because so there are so many different specs around, my mutation observer has an timeout of 1second for every loaded page / mutation because else the data’s are loaded to slow and it still has the data’s from the page before.
Is there a faster way sending these data’s around besides the chrome storage ?
Injected.js
//Gettig Datas before as constant players
const payload = {
PlayerList: players.map(player => {
return {
ID: player.id, //resource Id
Price: player.bin // Price
};
}),
};
var payload2 = Object.values(payload);
chrome.runtime.sendMessage(extId, {type: 'GetPlayerList', data: payload2});
background.js
chrome.runtime.onMessageExternal.addListener(
function (request, sender, sendResponse) {
if (request.type === "GetPlayerList") {
var playertest = request;
var playertest2 = playertest.data[0];
var playerbase = chrome.storage.local.get("datafut", function (data) {
playerbase = data.datafut;
var data = mergeArrays(playertest2, playerbase);
chrome.storage.local.set(
{
playerDataListCurrent: data
});
console.log(mergeArrays(playertest2, playerbase));
})
}
});
function mergeArrays(playertest2, playerbase) { //Calculate an array
by filter the IDs from a 30Element Array and a 500Element Array}
mergeArrays function: array
content.js
var s = document.createElement('script');
s.src = chrome.extension.getURL('injected.js');
s.dataset.variable = JSON.stringify(chrome.runtime.id);
s.asnyc = false; (document.head ||
document.documentElement).appendChild(s); s.onload = function () { s.remove(); };
var observeTransferList = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (node) {
if (node.nodeType === 1 && node.matches(".has-auction-data")) {
$(node).css("height", "28");
setTimeout(() => {
var playerDataListCurrent;
chrome.storage.sync.get(function (items) {
platform = items.platform;
discountprice = items.discountprice;
average = parseInt(items.average);
percentage = parseInt(items.percentage);
if (percentage === 0) {
//Data get
chrome.storage.local.get(function (items) {
playerDataListCurrent = items.playerDataListCurrent;
for (i = 0; i < playerDataListCurrent.length; i++) {
//DO STUFF
}
})
}, 1000); // Timeout for unknown delay. Else its sometimes getting datas from array calculated before
}
});
});
});
I've been working on this for days and I can't seem to find a solution.
I want this script to wait until the user presses the enter key after the first value has been inputted into the field. I want the script to keep doing this every time a value is added, but I can't quite seem to find out how to do this.
$(document).ready(function() {
console.log("script loaded");
var apiKey = "";
var itemImage = $(".title-wrap img");
var itemList = [];
var i = 0;
var addPage = false;
// Run through all images and grab all item ID's.
function scrapeItems() {
itemImage.each(function() {
var grabItemID = $(this).attr("src").match(/\d+/)[0];
var disabled = $(this).closest("li.clearfix").hasClass("disabled");
// Add item number as class for easy reference later.
$(this).addClass("item-" + grabItemID);
// If the item's row has "disabled" class, skip this item.
if (disabled) {
return true;
scrapeItems();
}
// Add item to array.
itemList.push(grabItemID);
});
}
scrapeItems();
// Call the API request function and start gathering all bazaar prices.
function getPricing() {
console.log("script started");
$.each(itemList, function(key, value) {
// Set three second timer per API request.
setTimeout(function() {
// Actual API request.
return $.ajax({
dataType: "json",
url: "https://api.torn.com/market/" + value,
data: {
selections: "bazaar",
key: apiKey
},
// When data is received, run this.
success: function(data) {
console.log(value + " request was successful");
var cheapest = null;
// Run through all results and return the cheapest.
$.each(data["bazaar"], function(key, val) {
var cost = val["cost"];
if (cheapest == null || cost < cheapest) {
cheapest = cost;
}
});
var inputMoney = $(".item-" + value).closest("li.clearfix").find(".input-money:text");
inputMoney.val(cheapest - 1).focus();
// I WANT THE FUNCTION TO WAIT HERE UNTIL THE USER PRESSES ENTER
},
// When data is not received, run this.
error: function() {
console.log(value + " request was NOT successful");
}
});
}, key * 3000);
});
}
function checkPage() {
var i = 0;
var url = window.location.href;
i++
setTimeout(function() {
if (url.indexOf("bazaar.php#/p=add") > 0) {
addPage = true;
addButton();
} else {
checkPage();
}
}, i * 1000);
}
checkPage();
function addButton() {
$("#inventory-container").prepend('<button id="start-button" style="margin-bottom:10px;margin-right:10px;">Run Auto-pricing script</button><p id="s-desc" style="display:inline-block;font-weight:bold;text-transform:uppercase;">Press the enter key after the price has shown up!</p>');
}
$(document).on("click", "#start-button", function() {
getPricing();
});
});
I'm at a complete loss on this one guys, so all help is appreciated!
I think you should break down your code a bit more, and move the "on enter" part of the code into a separate function instead of waiting for user input within that success callback.
e.g in pseudo code, different stages of the scraping
let priceData;
const preProcessPriceData = (data) => {
// do some pre-processing, validate, change data formats etc
// return processed data
};
const processPriceData = (data) => {
// called when price data is ready and user pressed enter
// in other words - script continues here
console.log(priceData, 'or', data);
};
scrapeItems();
// in get prices function - remove event handler
$("#some-input-user-is-pressing-enter-in").offOnEnter(processPriceData);
priceData = null;
getPrices().then((data) => {
priceData = data;
let processedData = preProcessPriceData(data);
// add listener to wait for user input
$("#some-input-user-is-pressing-enter-in").onEnter(() => {
// script continues after user presses enter
processPriceData(processedData);
});
});
I'm hitting an API which returns all details on kills in a game, the first endpoint returns an id to the kill event, then a second endpoint is hit to retrieve the killer and killed names.
Because of the way this API is set up I need to make a request to first get the event ID and then wait for all id's in the returned array to get a result and then process the entire kill array:
requestify.get(url).then(function (response) {
var events = [];
if (response.body && response.body.length > 0) {
data = JSON.parse(response.body);
if (data.hasOwnProperty('events')) {
events = data.events.map(function(event) {
return this.getDataForHeroKillId(event.id, function(killInfo) {
return { killer: killInfo.killer, killed: killInfo.killed, timestamp: event.time };
});
}.bind(this));
console.log('events is: ', events);
}
}
return Promise.all(events);
}.bind(this));
My getKillInformation function looks like this:
KillFeed.prototype.getKillInformation = function(id, cb) {
var data = null;
requestify.get(url).then(function (response) {
var event = {};
if (response.body && response.body.length > 0) {
data = JSON.parse(response.body);
if (data.hasOwnProperty('Killer')) {
event = { killer: data.Killer, killed: data.Killed};
}
}
cb(event);
});
};
In the second method I was hoping that I could callback the result of each child request and then once they had all been executed my new array would hold the data. But due to the event driven nature of JS I found that my code block continues to return an empty events array as this code is obviously non blocking (understandably as blocking the event queue whilst making a HTTP request is not ideal). How can I implement this?
One uses promises for this.
requestify.get(url).then(function (response) {
var events = [];
if (response.body && response.body.length > 0) {
var data = JSON.parse(response.body);
if (data.hasOwnProperty('events')) {
// Trying to process the kill information here
events = data.events.map(function(event) {
return this.getKillInformation(event.id).then(function(killInfo) {
return { killer: killInfo.killer, killed: killInfo.killed, timestamp: event['time1'] };
});
}.bind(this));
}
}
return Promise.all(events);
});
KillFeed.prototype.getKillInformation = function(id) {
var url = 'internal_url';
return requestify.get(url).then(function (response) {
if (response.body && response.body.length > 0) {
var data = JSON.parse(response.body);
if (data.hasOwnProperty('killer')) {
return { killer: data.Killer, killed: data.Killed };
}
}
});
};
You could use async and its waterfall method. Async is a NodeJS module, but it can be used in browser, too.
What I have is simple CRUD operation. Items are listed on page, when user clicks button add, modal pops up, user enters data, and data is saved and should automatically (without refresh)be added to the list on page.
Service:
getAllIncluding: function(controllerAction, including) {
var query = breeze.EntityQuery.from(controllerAction).expand(including);
return manager.executeQuery(query).fail(getFailed);
},
addExerciseAndCategories: function(data, initialValues) {
var addedExercise = manager.createEntity("Exercise", initialValues);
_.forEach(data, function(item) {
manager.createEntity("ExerciseAndCategory", { ExerciseId: addedExercise._backingStore.ExerciseId, CategoryId: item.CategoryId });
});
saveChanges().fail(addFailed);
function addFailed() {
removeItem(items, item);
}
},
Controller:
$scope.getAllExercisesAndCategories = function() {
adminCrudService.getAllIncluding("ExercisesAndCategories", "Exercise,ExerciseCategory")
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
$scope.queryItems = adminCrudService.querySucceeded(data);
var exerciseIds = _($scope.queryItems).pluck('ExerciseId').uniq().valueOf();
$scope.exerciseAndCategories = [];
var createItem = function (id, exercise) {
return {
ExerciseId: id,
Exercise : exercise,
ExerciseCategories: []
};
};
// cycle through ids
_.forEach(exerciseIds, function (id) {
// get all the queryItems that match
var temp = _.where($scope.queryItems, {
'ExerciseId': id
});
// go to the next if nothing was found.
if (!temp.length) return;
// create a new (clean) item
var newItem = createItem(temp[0].ExerciseId, temp[0].Exercise);
// loop through the queryItems that matched
_.forEach(temp, function (i) {
// if the category has not been added , add it.
if (_.indexOf(newItem.ExerciseCategories, i.ExerciseCategory) < 0) {
newItem.ExerciseCategories.push(i.ExerciseCategory);
}
});
// Add the item to the collection
$scope.items.push(newItem);
});
$scope.$apply();
}
Here is how I add new data from controller:
adminCrudService.addExerciseAndCategories($scope.selectedCategories, { Name: $scope.NewName, Description: $scope.NewDesc });
So my question is, why list isn't updated in real time (when I hit save I must refresh page).
EDIT
Here is my querySuceeded
querySucceeded: function (data) {
items = [];
data.results.forEach(function(item) {
items.push(item);
});
return items;
}
EDIT 2
I believe I've narrowed my problem !
So PW Kad lost two hours with me trying to help me to fix this thing (ad I thank him very very very much for that), but unfortunately with no success. We mostly tried to fix my service, so when I returned to my PC, I've again tried to fix it. I believe my service is fine. (I've made some changes as Kad suggested in his answer).
I believe problem is in controller, I've logged $scope.items, and when I add new item they don't change, after that I've logged $scope.queryItems, and I've noticed that they change after adding new item (without refresh ofc.). So probably problem will be solved by somehow $watching $scope.queryItems after loading initial data, but at the moment I'm not quite sure how to do this.
Alright, I am going to post an answer that should guide you on how to tackle your issue. The issue does not appear to be with Breeze, nor with Angular, but the manner in which you have married the two up. I say this because it is important to understand what you are doing in order to understand the debug process.
Creating an entity adds it to the cache with an entityState of isAdded - that is a true statement, don't think otherwise.
Now for your code...
You don't have to chain your query execution with a promise, but in your case you are returning the data to your controller, and then passing it right back into some function in your service, which wasn't listed in your question. I added a function to replicate what yours probably looks like.
getAllIncluding: function(controllerAction, including) {
var query = breeze.EntityQuery.from(controllerAction).expand(including);
return manager.executeQuery(query).then(querySucceeded).fail(getFailed);
function querySucceeded(data) {
return data.results;
}
},
Now in your controller simply handle the results -
$scope.getAllExercisesAndCategories = function() {
adminCrudService.getAllIncluding("ExercisesAndCategories", "Exercise,ExerciseCategory")
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
// Set your object directly to the data.results, because that is what we are returning from the service
$scope.queryItems = data;
$scope.exerciseAndCategories = [];
Last, let's add the properties we create the entity and see if that gives Angular a chance to bind up properly -
_.forEach(data, function(item) {
var e = manager.createEntity("ExerciseAndCategory");
e.Exercise = addedExercise; e.Category: item.Category;
});
So I've managed to solve my problem ! Not sure if this is right solution but it works now.
I've moved everything to my service, which now looks like this:
function addCategoriesToExercise(tempdata) {
var dataToReturn = [];
var exerciseIds = _(tempdata).pluck('ExerciseId').uniq().valueOf();
var createItem = function (id, exercise) {
return {
ExerciseId: id,
Exercise: exercise,
ExerciseCategories: []
};
};
// cycle through ids
_.forEach(exerciseIds, function (id) {
// get all the queryItems that match
var temp = _.where(tempdata, {
'ExerciseId': id
});
// go to the next if nothing was found.
if (!temp.length) return;
// create a new (clean) item
var newItem = createItem(temp[0].ExerciseId, temp[0].Exercise);
// loop through the queryItems that matched
_.forEach(temp, function (i) {
// if the category has not been added , add it.
if (_.indexOf(newItem.ExerciseCategories, i.ExerciseCategory) < 0) {
newItem.ExerciseCategories.push(i.ExerciseCategory);
}
});
// Add the item to the collection
dataToReturn.push(newItem);
});
return dataToReturn;
}
addExerciseAndCategories: function (data, initialValues) {
newItems = [];
var addedExercise = manager.createEntity("Exercise", initialValues);
_.forEach(data, function (item) {
var entity = manager.createEntity("ExerciseAndCategory", { ExerciseId: addedExercise._backingStore.ExerciseId, CategoryId: item.CategoryId });
items.push(entity);
newItems.push(entity);
});
saveChanges().fail(addFailed);
var itemsToAdd = addCategoriesToExercise(newItems);
_.forEach(itemsToAdd, function (item) {
exerciseAndCategories.push(item);
});
function addFailed() {
removeItem(items, item);
}
}
getAllExercisesAndCategories: function () {
var query = breeze.EntityQuery.from("ExercisesAndCategories").expand("Exercise,ExerciseCategory");
return manager.executeQuery(query).then(getSuceeded).fail(getFailed);
},
function getSuceeded(data) {
items = [];
data.results.forEach(function (item) {
items.push(item);
});
exerciseAndCategories = addCategoriesToExercise(items);
return exerciseAndCategories;
}
And in controller I have only this:
$scope.getAllExercisesAndCategories = function () {
adminExerciseService.getAllExercisesAndCategories()
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
$scope.items = data;
$scope.$apply();
}
I have quite a few scenarios where I need clicks, etc. to trigger behavior in another place on the page (a one-way communication scenario). I now have a need for bi-directional communication, where stuff that happens in element A can modify specific properties in the scope behind element B and vice-versa. Thus far, I've been using $rootScope.$broadcast to facilitate this but it feels like overkill, and winds up creating boilerplate in both places:
$scope.$on('event-name', function(event, someArg) {
if(someArg === $scope.someProperty) return;
$scope.someProperty = someArg;
});
$scope.$watch('someProperty', function(newValue) {
$rootScope.$broadcast('event-name', newValue);
});
Is there a better way? I'd like to tie the two (or three, or N) scopes together via a service, but I don't see a way to do that without magic event names and boilerplate.
I haven't used this myself, but this post explains basically how I would do it. Here's the code which illustrates the idea:
(function() {
var mod = angular.module("App.services", []);
//register other services here...
/* pubsub - based on https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js*/
mod.factory('pubsub', function() {
var cache = {};
return {
publish: function(topic, args) {
cache[topic] && $.each(cache[topic], function() {
this.apply(null, args || []);
});
},
subscribe: function(topic, callback) {
if(!cache[topic]) {
cache[topic] = [];
}
cache[topic].push(callback);
return [topic, callback];
},
unsubscribe: function(handle) {
var t = handle[0];
cache[t] && d.each(cache[t], function(idx){
if(this == handle[1]){
cache[t].splice(idx, 1);
}
});
}
}
});
return mod;
})();
Note the memory leak though if controllers are "deleted" without unsubscribing.
I think you can try the following service,
'use strict';
angular.module('test')
.service('messageBus', function($q) {
var subscriptions = {};
var pendingQuestions = [];
this.subscribe = function(name) {
subscriptions[name].requestDefer = $q.defer();
return subscriptions[name].requestDefer.promise; //for outgoing notifications
}
this.unsubscribe = function(name) {
subscriptions[name].requestDefer.resolve();
subscriptions[name].requestDefer = null;
}
function publish(name, data) {
subscriptions[name].requestDefer.notify(data);
}
//name = whom shd answer ?
//code = what is the question ?
//details = details abt question.
this.request = function(name, code, details) {
var defered = null;
if (subscriptions[name].requestDefer) {
if (pendingQuestions[code]) {
//means this question is already been waiting for answer.
//hence return the same promise. A promise with multiple handler will get
//same data.
defered = pendingQuestions[code];
} else {
defered = $q.defer();
//this will be resolved by response method.
pendingQuestions[code] = defered;
//asking question to relevant controller
publish(name, {
code: code,
details: details
});
}
} else {
//means that one is not currently in hand shaked with service.
defered = $q.defer();
defered.resolve({
code: "not subscribed"
});
}
return defered.promise;
}
//data = code + details
//responder does not know the destination. This will be handled by the service using
//pendingQuestions[] array. or it is preemptive, so decide by code.
this.response = function(data) {
var defered = pendingQuestions[data.code];
if (defered) {
defered.resolve(data);
} else {
//means nobody requested for this.
handlePreemptiveNotifications(data);
}
}
function handlePreemptiveNotifications() {
switch (data.code) {
//handle them case by case
}
}
});
This can be used as a message bus in multi controller communication. It is making use of the angular notify() callback of promise API.All the participating controllers should subscribe the service as follows,
angular.module('test')
.controller('Controller1', function($scope, messageBus) {
var name = "controller1";
function load() {
var subscriber = messageBus.subscribe(name);
subscriber.then(null, null, function(data) {
handleRequestFromService(data);
});
}
function handleRequestFromService(data) {
//process according to data content
if (data.code == 1) {
data.count = 10;
messageBus.respond(data);
}
}
$scope.$on("$destroy", function(event) {
//before do any pending updates
messageBus.unsubscribe(name);
});
load();
});
angular.module('test')
.controller('Controller2', function($scope, messageBus) {
var name = "controller2";
function load() {
var subscriber = messageBus.subscribe(name);
subscriber.then(null, null, function(data) {
handleRequestFromService(data);
});
}
function handleRequestFromService(data) {
//process according to data content
}
$scope.getHorseCount = function() {
var promise = messageBus.request("controller1", 1, {});
promise.then(function(data) {
console.log(data.count);
});
}
$scope.$on("$destroy", function(event) {
//before do any pending updates
messageBus.unsubscribe(name);
});
load();
});