I'm trying to create an array of objects from the return of a $resource query as shown in this SO question: link. However, I keep getting the same list of Resources and other elements. I have a plunk: here (You have to open the developer console to see the output.)
var app = angular.module('plunker', ['ngResource']);
app.factory('NameResource', function($resource) {
var url = 'data.json';
var res = $resource(url, null, {
query: {
method: 'GET',
isArray: true,
transformResponse: function(data, headersGetter) {
var items = angular.fromJson(data);
var models = [];
angular.forEach(items, function(item) {
models.push(item);
});
console.log("models: ", models);
return models;
}
}
});
return res;
});
app.controller('MainCtrl', function($scope, NameResource) {
$scope.names = NameResource.query();
console.log('Inside controller: ', $scope.names);
setTimeout(function(){console.log('after some time names is:', $scope.names)}, 3000);
});
What am I doing wrong? Or have I misunderstood something. Also what is the difference between the two? It seems to work very similar for me. When will it cause an issue?
Resource.query returns an array (because of the isArray flag you created) with two properties, $promise which is the promise which when resolved will "copy" all the response values to the array returned from Resource.query magically updating the view and $resolved which is a flag telling if $promise was resolved already, to answer your question there's actually some additional transformation happening, the data returned from your transformation will actually go through another transform (which can't be disabled) and this is where your each object is transformed into a Resource instance.
So this is what you're expecting to happen:
promise
.then(function (rawData) {
// this is where your transformation is happening
// e.g. transformResponse is called with rawData
// you return your transformed data
})
.then(function (transformedData) {
// raw data has gone through 1 transformation
// you have to decide what to do with the data, like copying it to
// some variable for example $scope.names
})
But Resource is doing the following:
promise
.then(function (rawData) {
// this is where your transformation is happening
})
.then(function (transformedData) {
// Resource is adding this handler and doing the
// 'copy' to array operation here for you,
// but it will actually create a Resource instance
// in the process with each item of your array!
})
.then(function (transformedDataV2) {
// raw data has gone through 2 transformations here!
})
The additional transformation is where the magic happens and is where the Resource instance is created, if we take a look at the source code these are the lines which take care of this transformation, I'll copy them here:
if (action.isArray) {
value.length = 0;
forEach(data, function(item) {
if (typeof item === "object") {
value.push(new Resource(item));
} else {
// Valid JSON values may be string literals, and these should not be converted
// into objects. These items will not have access to the Resource prototype
// methods, but unfortunately there
value.push(item);
}
});
}
data is the data returned by your first transformation and as seen above it'll pass the typeof item === 'Object' check so value which is the array returned by Resource.query is updated with a new Resource item (not with item). You were worried about this strange Resource object, let's analyze the Resource constructor:
function Resource(value) {
shallowClearAndCopy(value || {}, this);
}
It's just copying each of the properties of the object value to this (this is the new Resource instance), so now we're dealing with Resource objects and not plain array objects
will it cause an issue?
I'm sure it will, if the transform function you define is a little bit more complex like having each object actually be an instance of something else whose __proto__ has some methods, e.g. a Person object instead of a plain object then the methods defined in Person.prototype won't be visible to the result of the whole operation since each object won't be a Person instance but a Resource instance! (see this error in this plunkr, make sure to read the comments and also look at the error raised in the console because of the undefined method)
Related
This is a little bit tricky to explain, but I'll give it a try:
In a node.js server application I would like to deal with data objects that can be used in more than one place at once. The main problem is, that these objects are only referred to by an object id and are loaded from the database.
However, as soon as an object is already loaded into one scope, it should not be loaded a second time when requested, but instead the same object should be returned.
This leads me to the question of garbage collection: As soon as an object is no longer needed in any scope, it should be released completely to prevent having the whole database in the server's memory all the time. But here starts the problem:
There are two ways I can think of to create such a scenario: Either use a global object reference (which prevents any object from being collected) or, really duplicate these objects but synchronize them in a way that each time a property in one scope gets changed, inform the other instances about that change.
Again, therefore each instance would have to register an event handler, which in turn is pointing back to that instance thus preventing it from being collected again.
Did anyone come up with a solution for such a scenario I just didn't realize? Or is there any misconception in my understanding of the garbage collector?
What I want to avoid is manual reference counting for every object in the memory. Everytime when an object is being removed from any collection, I would have to adapt the reference count manually (there is even no destructor or "reference decreased" event in js)
Using the weak module, I implemented a WeakMapObj that works like we originally wanted WeakMap to work. It allows you to use a primitive for the key and an object for the data and the data is retained with a weak reference. And, it automatically removes items from the map when their data is GCed. It turned out to be fairly simple.
const weak = require('weak');
class WeakMapObj {
constructor(iterable) {
this._map = new Map();
if (iterable) {
for (let array of iterable) {
this.set(array[0], array[1]);
}
}
}
set(key, obj) {
if (typeof obj === "object") {
let ref = weak(obj, this.delete.bind(this, key));
this._map.set(key, ref);
} else {
// not an object, can just use regular method
this._map.set(key, obj);
}
}
// get the actual object reference, not just the proxy
get(key) {
let obj = this._map.get(key);
if (obj) {
return weak.get(obj);
} else {
return obj;
}
}
has(key) {
return this._map.has(key);
}
clear() {
return this._map.clear();
}
delete(key) {
return this._map.delete(key);
}
}
I was able to test it in a test app and confirm that it works as expected when the garbage collector runs. FYI, just making one or two objects eligible for garbage collection did not cause the garbage collector to run in my test app. I had to forcefully call the garbage collector to see the effect. I assume that would not be an issue in a real app. The GC will run when it needs to (which may only run when there's a reasonable amount of work to do).
You can use this more generic implementation as the core of your object cache where an item will stay in the WeakMapObj only until it is no longer referenced elsewhere.
Here's an implementation that keeps the map entirely private so it cannot be accessed from outside of the WeakMapObj methods.
const weak = require('weak');
function WeakMapObj(iterable) {
// private instance data
const map = new Map();
this.set = function(key, obj) {
if (typeof obj === "object") {
// replace obj with a weak reference
obj = weak(obj, this.delete.bind(this, key));
}
map.set(key, obj);
}
// add methods that have access to "private" map
this.get = function(key) {
let obj = map.get(key);
if (obj) {
obj = weak.get(obj);
}
return obj;
}
this.has = function(key) {
return map.has(key);
}
this.clear = function() {
return map.clear();
}
this.delete = function(key) {
return map.delete(key);
}
// constructor implementation
if (iterable) {
for (let array of iterable) {
this.set(array[0], array[1]);
}
}
}
Sounds like a job for a Map object used as a cache storing the object as the value (along with a count) and the ID as the key. When you want an object, you first look up its ID in the Map. If it's found there, you use the returned object (which will be shared by all). If it's not found there, you fetch it from the database and insert it into the Map (for others to find).
Then, to make it so that the Map doesn't grow forever, the code that fetches something from the Map would also need to release an object from the Map. When the useCnt goes to zero upon a release, you would remove an object from the Map.
This can be made entirely transparent to the caller by creating some sort of cache object that contains the Map and has methods for getting an object or releasing an object and it would be entirely responsible for maintaining the refCnt on each object in the Map.
Note: you will likely have to write the code that fetches it from the DB and inserts it into the Map carefully in order to not create a race condition because the fetching form the database is likely asynchronous and you could get multiple callers all not finding it in the Map and all in the process of getting it from the database. How to avoid that race condition depends upon the exact database you have and how you're using it. One possibility is for the first caller to insert a place holder in the Map so subsequent callers will know to wait for some promise to resolve before the object is inserted in the Map and available to them to use.
Here's a general idea for how such an ObjCache could work. You call cache.get(id) when you want to retrieve an item. This always returns a promise that resolves to the object (or rejects if there's an error getting it from the DB). If the object is in the cache already, the promise it returns will be already resolved. If the object is not in the cache yet, the promise will resolve when it has been fetched from the DB. This works even when multiple parts of your code request an object that is "in the process" of being fetched from the DB. They all get the same promise that is resolved with the same object when the object has been retrieved from the DB. Every call to cache.get(id) increases the refCnt for that object in the cache.
You then call cache.release(id) when a given piece of code is done with an object. That will decrement the internal refCnt and remove the object from the cache if the refCnt hits zero.
class ObjCache() {
constructor() {
this.cache = new Map();
}
get(id) {
let cacheItem = this.cache.get(id);
if (cacheItem) {
++cacheItem.refCnt;
if (cacheItem.obj) {
// already have the object
return Promise.resolve(cacheItem.obj);
}
else {
// object is pending, return the promise
return cacheItem.promise;
}
} else {
// not in the cache yet
let cacheItem = {refCnt: 1, promise: null, obj: null};
let p = myDB.get(id).then(function(obj) {
// replace placeholder promise with actual object
cacheItem.obj = obj;
cacheItem.promise = null;
return obj;
});
// set placeholder as promise for others to find
cacheItem.promise = p;
this.cache.set(id, cacheItem);
return p;
}
}
release(id) {
let cacheItem = this.cache.get(id);
if (cacheItem) {
if (--cacheItem.refCnt === 0) {
this.cache.delete(id);
}
}
}
}
Ok, for anyone who faces similar problems, I found a solution. jfriend00 pushed me towards this solution by mentioning WeakMaps which were not exactly the solution themselves, but pointed my focus on weak references.
There is an npm module simply called weak that will do the trick. It holds a weak reference to an object and safely returns an empty object once the object was garbage collected (thus, there is a way to identify a collected object).
So I created a class called WeakCache using a DataObject:
class DataObject{
constructor( objectID ){
this.objectID = objectID;
this.dataLoaded = new Promise(function(resolve, reject){
loadTheDataFromTheDatabase(function(data, error){ // some pseudo db call
if (error)
{
reject(error);
return;
}
resolve(data);
});
});
}
loadData(){
return this.dataLoaded;
}
}
class WeakCache{
constructor(){
this.cache = {};
}
getDataObjectAsync( objectID, onObjectReceived ){
if (this.cache[objectID] === undefined || this.cache[objectID].loadData === undefined){ // object was not cached yet or dereferenced, recreate it
this.cache[objectID] = weak(new DataObject( objectID )function(){
// Remove the reference from the cache when it got collected anyway
delete this.cache[this.objectID];
}.bind({cache:this, objectID:objectID});
}
this.cache[objectID].loadData().then(onObjectReceived);
}
}
This class is still in progress but at least this is a way how it could work. The only downside to this (but this is true for all database-based data, pun alert!, therefore not such a big deal), is that all data access has to be asynchronous.
What will happen here, is that the cache at some point may hold an empty reference to every possible object id.
I am trying to get songs from soundcloud, I am using some input to set value and send it to my factory to get all the related list of songs and display it.
The issue is the the first time all works correctly, but when I am trying to input new values I am getting same results as first time.
My code looks like:
.controller('DashCtrl', function ($scope, SongsService) {
$scope.formData = {};
$scope.searchSong = function () {
SongsService.setData($scope.formData.songName);
};
UPDATE
the factory :
.factory('SongsService', function ($rootScope) {
var List = {};
List.setData = function (tracks) {
var page_size = 6;
SC.get('/tracks', {limit: page_size, linked_partitioning: 1}, function (tracks) {
// page through results, 100 at a time
List = tracks;
$rootScope.$broadcast('event:ItemsReceived');
});
};
List.getItems = function () {
return List;
};
return List;
}).value('version', '0.1');
Thanks for help!
It's hard to tell without a plunkr reproducing the issue and showing all your relevant code, but I think your problem is that you're overwriting the List variable in the async answer, and this List (I assume) is the object you originally returned from your factory.
I see two noteworthy concepts here:
the fact that angular factories are effectively singletons
and that javascript objects are passed by reference-by-value (see call-by-sharing, or one of many stackoverflow discussions).
An angular factory is a singleton, meaning the factory function will only be called once, before the first injection, and every controller it's injected into will work with the same object reference it returned. If you overwrite this object reference, well, the previous value (which the controller has) is still a reference to the original object.
Edit: In fact, by overwriting List you're creating a new object which doesn't even have a setData method anymore!
You probably want to make List private to SongsService, and return a more complex object from the factory that captures List in a closure, and offers some public getter/setter methods for it. (If you insist on replacing the contents of the returned List variable, empty the object and extend it with the new properties - including this method again. But this is much more work, and not a nice solution.)
In Angular Service constructors and Factory methods are singleton objects. You need to return a method that you can call. Your code examples are incomplete so it is hard to tell what is going on. What is returned by your factory method, the List object?
If so, when the first call is completed, it overwrites the List object so that the setData method can't be called a second time. What is the SC object, I can not see in your example how you are injecting it. You probably want to fix that too.
Consider this possible solution.
Service
Songs.$inject = ['$http'];
function Songs($http) {
this.$http = $http;
}
Songs.prototype.getSongs = function(searchTerm) {
return this.$http.get('http://someendpoint/songs', {searchTerm: searchTerm});
}
service('songs', Songs);
Controller
DashController.$inect = ['songs'];
functionDashController(songs) {
this.songs = songs;
this.results = [];
}
DashController.prototype.searchSongs = function(searchTerm) {
var self = this;
this.songs.getSongs(searchTerm).then(function(results) {
this.results = results;
});
}
controller('DashController', DashController);
This is example uses the best practice controllerAs syntax explained here: http://toddmotto.com/digging-into-angulars-controller-as-syntax/
I found the issue,
I got same results all the time because I didnt use cooreclty the api of soundcloud, I didnt send the title on the api... also you are correct, I should not set the list as empty..I should set some value to the list...
I have a static file that's just one date-time per line, e.g.:
2014-03-14T16:32
2014-03-15T13:04
2014-03-16T06:44
...
I want to use that as a read-only data source for a backbone collection. Obviously that's not in the expected format. I thought I could just use the parse method on my collection to just convert it into a proper array of objects. Unfortunately, that doesn't seem to be working.
First, if I don't override fetch, the parse method never gets called -- it goes into the error handler, though it's unlcear exactly why -- it's not actually throwing any errors. I'm guessing that's because it's expecting a different response type or something?
Second, if I override both the fetch & parse methods thus:
var MyColl = Backbone.Collection.extend({
model: MyModel,
url: 'date-list.txt',
parse: function(data) {
var result = _(data.split("\n")).compact().map(function(item, i) {
return { theDate: item, globalIndex: i };
});
return result;
},
fetch: function() {
$.get(this.url, this.parse);
}
});
it correctly goes into parse, and parse seems to build a valid object, but my collection has 0 models after the whole operation...
I suppose the collection is winding up empty because when I call parse, it's not part of the expected flow any more so nothing is done with the result. Any other ideas about how I can make fetch properly handle what the server is returning?
Obviously, I could make the server return JSON, or I could use my own fetching function outside of backbone, but I'm hoping there's a way to avoid those ideas.
I believe the default fetch method assumes you will be returning JSON from the endpoint, which the collection will then instantiate a new model for each object in the array from the JSON data.
With your current override, you just need to just build an array of Backbone models in your parse method:
parse: function(data) {
var model = this.model;
var result = _(data.split("\n")).compact().map(function(item, i) {
return new model({ theDate: item, globalIndex: i });
});
return result;
},
It looks like you are not passing correct function to $.get, or, to be more precise, correct function but not bound to specific object instance. One idea is to try this:
$.get(this.url, _.bind(this.parse, this));
But now, as you said, nothing is done with the result of parse method, so you can add elements to collection like this:
parse: function(data) {
var result = _(data.split("\n")).compact().map(function(item, i) {
return { theDate: item, globalIndex: i };
});
this.set(result);
}
I'm trying to alter the folders variable ( an array) within the return statement of my angular factory but it only seems to work for the create action I wrote. I think I'm not understanding the way variable scope is working in the factory.
this works:
folders.push(response.folder)
but... when I use the underscore.js function _without (that returns an array), it does not alter the folder variable.
folders = _.without(folders, _.findWhere(folders, {id: folder.id }))
Here is my factory code:
angular.module('cmsApp')
.factory('Folders', function(Restangular){
var folders = Restangular.all('folders').getList().$object;
return {
get: function(){
return folders;
},
create: function(folderName) {
folder = { label: folderName };
folders.post(folder).then(function(response) {
folders.push(response.folder);
})
return folders;
},
destroy: function(folder) {
folders.remove(folder).then(function(){
folders = _.without(folders, _.findWhere(folders, {id: folder.id }))
})
return folders;
}
}
});
The create function returns an updated folders var but the destroy function returns an un-altered folders var.
_.without returns a new array, see docs, so you are effectively changing where folders is pointing to. However, since you are calling _.without only once the promise resolves, i.e. inside then this change will occur only after you have returned the original folders object so you end up with two references pointing to different arrays.
It's probably best practice to return the actual promise from inside your factory, this way you can reference the correct data once the promise returns.
here is my JavaScript code:
var Model =
{
get: function(id)
{
return this.data[id];
},
data: {},
init: function()
{
var self = this;
$.getJSON(urlToServer, function(data)
{
$.each(data, function(i, object)
{
self.data[object.id] = object;
console.log(object.id); // output is: 1, then 2, then 3
});
});
}
};
Model.init();
console.log(Model); // output is the initialized object with children objects 1, 2, 3
console.log(Model.get(1)); // output is undefined
As you can see from the console output i put in the comments, everything works fine until the last line of code. I define a Model and initialize it with some JSON objects provided by the server. But all of a sudden, when i try to access a single child object through the get() method, the Model appears to be undefined.
I just don't get it, please help me out.
Thanks.
Looking at the sample code you used, Model.get(1) will always return undefined.
$.getJSON is an AJAX call that does not necessarily return immediately (known as asynchronous). You will need to use the callback you supplied to $.getJSON to fire off any logic depending on Model.get(1), otherwise it will remain undefined.
$.getJSON is a asynchronous request, you must wait for the response before you call Model.get()
You trying to retrieve object's field "142". I guess you get from json only "1", "2" and "3" id's? If i'm right then get function return to you correct answer because no object field "142" exists.