converting array of paths to array of unique paths - javascript

I have an array of paths that i want to convert to an array of unique paths. Below is my attempt and i know there is a recursive call somewhere in function returnPath but don't know exactly how to implement that.
var paths = [{
out: "KFvEbaKPDC|o|0",
in: "M7gtymFAo4|i|0"
}, {
out: "KFvEbaKPDC|o|0",
in: "53s1L4YLpo|i|0"
}, {
out: "M7gtymFAo4|o|0",
in: "DEa78KAyDf|i|0"
}, {
out: "53s1L4YLpo|o|0",
in: "R6hUSNn5T5|i|0"
}]
function chainPaths(paths) {
let localCopy = paths;
const chainPaths = [];
localCopy = localCopy.filter((item) => {
if (item.out.split('|')[0] === 'KFvEbaKPDC') {
return item;
}
})
console.log(localCopy);
localCopy.forEach((c) => {
chainPaths.push(returnUniquePath(c));
})
}
function returnUniquePath(c) {
const arr = [];
arr.push(c.out);
arr.push(c.in);
//recursive call here may be??
return arr;
}
chainPaths(paths)
i am hoping to end up with an array that has unique paths like below
[[
"KFvEbaKPDC|o|0",
"M7gtymFAo4|i|0"
"M7gtymFAo4|o|0",
"DEa78KAyDf|i|0"
],[
"KFvEbaKPDC|o|0",
"53s1L4YLpo|i|0"
"53s1L4YLpo|o|0",
"R6hUSNn5T5|i|0"
]]
lets imagine 'KFvEbaKPDC' to be a parent node, this parent has output path, denoted by 'o' as in KFvEbaKPDC|o|0, to two children 'M7gtymFAo4' and '53s1L4YLpo' on their inputs denoted by 'i' and each child i.e. M7gtymFAo4' and '53s1L4YLpo' has an output path to another node. Hope that makes sense.
one can visualize unique paths as below
KFvEbaKPDC|o|0 -> M7gtymFAo4|i|0 -> M7gtymFAo4|o|0 -> DEa78KAyDf|i|0
KFvEbaKPDC|o|0 -> 53s1L4YLpo|i|0 -> 53s1L4YLpo|o|0 -> R6hUSNn5T5|i|0
I was able to treeify the paths array and then was able to traverse unique paths like
KFvEbaKPDC -> M7gtymFAo4 -> DEa78KAyDf
but that solution lacked information about inputs, outputs and indices

Related

Make sure no duplicate directory paths exist

I'm writing a script wherein the user selects directories, which are then stored in an array property, so that they can be recursively crawled.
{
"archives": [
"C:\\AMD\\Packages",
"C:\\Users",
"C:\\Windows",
"D:\\",
"E:\\Pictures\\Birthday"
]
}
I obviously don't want to be storing duplicate paths or paths that are contained by other paths. For example, if the user were to select a new folder to add to the array, E:\\Pictures, then E:\\Pictures\\Birthday would be discarded and replaced by it since E:\\Pictures contains E:\\Pictures\\Birthday.
{
"archives": [
"C:\\AMD\\Packages",
"C:\\Users",
"C:\\Windows",
"D:\\",
"E:\\Pictures"
]
}
I know this can be done by parsing all of the values being considered (i.e. ['C:', 'AMD', 'Packages'], [...], ... etc) and then comparing them all to one another. However, this seems extremely intensive, especially if the array of paths grows bigger and the directory paths are longer.
You could also do it by comparing the strings with includes. For example, if A includes B or B includes A, split them, and discard the one with a longer length.
for (const dir of dirs){
if (newPath.includes(dir) || dir.includes(newPath)){
if (newPath.split('\\') < dir.split('\\')){
// remove dir from json object and replace it with newPath
}
} else {
pathArray.push(dir)
}
}
After reading one of the answers below, I just realized that the includes method runs into the issue of comparing similar, yet unique paths i.e. C:\Users and C:\User.
Although there's gotta be a better way to do this??
This function will give you your desired results. It first looks to see if the parent of the path exists in the archives, and if so, does nothing. If it doesn't, it then removes any children of the path and then inserts the new path.
Update
I've added a delim input to the function to make it usable for unix/MacOS style filenames as well.
let data = {
"archives": [
"C:\\AMD\\Packages",
"C:\\Users",
"C:\\Windows",
"D:\\",
"E:\\Pictures"
]
};
const add_to_archives = (path, data, delim) => {
// does the parent of this path already exist? if so, nothing to do
if (data.archives.reduce((c, v) =>
c || path.indexOf(v.slice(-1) == delim ? v : (v + delim)) === 0, false)) return data;
// not found. remove any children of this path
data.archives = data.archives.filter(v => v.indexOf(path.slice(-1) == delim ? path : (path + delim)) !== 0);
// and add the new path
data.archives.push(path);
return data;
}
add_to_archives("E:\\Pictures\\Too", data, "\\");
console.log(data);
add_to_archives("E:\\PicturesToo", data, "\\");
console.log(data);
add_to_archives("D:\\Documents", data, "\\");
console.log(data);
add_to_archives("C:\\AMD", data, "\\");
console.log(data);
data = {
"archives": [
"/var/www/html/site",
"/etc",
"/usr/tim",
"/bin"
]
};
add_to_archives("/var/www/html/site2", data, "/");
console.log(data);
add_to_archives("/etc/conf.d", data, "/");
console.log(data);
add_to_archives("/usr", data, "/");
console.log(data);
add_to_archives("/var/www/html", data, "/");
console.log(data);
.as-console-wrapper {
max-height: 100% !important;
}
We can approach the problem by using a prefix tree
The purpose is to limit the number of paths we check for inclusion or "containment".
That approach may be useful if you have a lot of siblings (tree traversal + lookup as key for each folder).
It is overkill if you often have a root folder specified in archives
algorithm
tree = {}
foreach path
split the path in folders (one may iterate with substring but it is worth it?)
try to match folders of that path while traversing the tree
if you encounter a stop node, skip to next path
if not,
if your path end on an existing node
mark that node as a stop node
drop the children of that node (you can let them be, though)
else
include the remaining folders of the path as node in tree
mark the last node as a stop node
Implem
Note that implem below will fail if path includes a folder named "stop". By subjective order of preference
Use Map and Symbol('stop')
or a real tree (at least do not store folders alongside the boolean stop)
do not suppose any stop node and always drop children if you manage to reach the end of your path
Hope no one tries to outsmart you and rename stop as some obscure -folder will not exist- lolol_xxzz9_stop
function nodupes(archives){
tree = {};
archives.forEach(arch=>{
const folders = arch.split('\\');
folders.splice(1,1);
//case of empty string such as D:\\\
if(folders[folders.length-1].length==0){folders.pop();}
let cur = tree;
let dropped = false;
let lastFolderIndex = 0;
let ok = folders.every((folder,i)=>{
if(cur[folder]){
if(cur[folder].stop){
dropped = true;
return false;
}
cur = cur[folder];
return true;
}
cur[folder] = {}
cur = cur[folder];
lastFolderIndex = i;
return true;
});
if(ok){
cur.stop = true;
//delete (facultatively) the subfolders
if(lastFolderIndex < folders.length-1){
console.log('cleanup', folders, 'on node', cur)
Object.keys(cur).forEach(k=>{
if(k != 'stop'){
delete cur[k];
}
})
}
}
});
//console.log('tree', JSON.stringify(tree,null,1));
//dfs on the tree to get all the paths to... the leaves
let dfs = function(tree,paths,p){
if(tree.stop){
return paths.push(p.join('\\\\'));
}
Object.keys(tree).forEach(k=>{
dfs(tree[k], paths, p.concat(k));
});
}
let paths = [];
dfs(tree, paths,[]);
return paths;
}
let archives = [
'C:\\\\ab',
'D:\\\\', //just some root
'D:\\\\ab',//be dropped
'D:\\\\abc',//dropped as well
'F:\\\\abc\\\\e',//two folder creation
'C:\\\\ab\\c',
'B:\\\\ab\\c',
'B:\\\\ab',//expect B\\\\ab\\c to be dropped
]
console.log(nodupes(archives))
Try this
console.log([
"C:\\AMD\\Packages",
"C:\\Users",
"C:\\User",
"E:\\Pictures",
"E:\\Pictures\\Birthday",
"C:\\Windows",
"D:\\",
"D:\\aabbcc",
"E:\\Pictures\\Birthday"
].sort().reduce(
(acc, cur) =>
acc.length > 0
&& cur.startsWith(acc[acc.length - 1])
&& ( cur.indexOf("\\", acc[acc.length - 1].replace(/\\$/,"").length) !== -1 )
&& acc || acc.concat(cur)
, []
))

dc.js Using two reducers without a simple dimension and second grouping stage

Quick question following up my response from this post:
dc.js Box plot reducer using two groups
Just trying to fully get my head around reducers and how to filter and collect data so I'll step through my understanding first.
Data Format:
{
"SSID": "eduroam",
"identifier": "Client",
"latitude": 52.4505,
"longitude": -1.9361,
"mac": "dc:d9:16:##:##:##",
"packet": "PR-REQ",
"timestamp": "2018-07-10 12:25:26",
"vendor": "Huawei Technologies Co.Ltd"
}
(1) Using the following should result in an output array of key value pairs (Key MAC Address & Value Count of networks connected to):
var MacCountsGroup = mac.group().reduce(
function (p, v) {
p[v.mac] = (p[v.mac] || 0) + v.counter;
return p;
},
function (p, v) {
p[v.mac] -= v.counter;
return p;
},
function () {
return {}; // KV Pair of MAC -> Count
}
);
(2) Then in order to use the object this must be passed flattened so it can be passed to a chart as follows:
function flatten_object_group(group) {
return {
all: function () {
return group.all().map(function (kv) {
return {
key: kv.key,
value: Object.values(kv.value).filter(function (v) {
return v > 0;
})
};
});
}
};
}
var connectionsGroup = flatten_object_group(MacCountsGroup);
(3) Then I pass mac as a piechart dimension & connectionsGroup as the group. This gives a chart back a chart with roughly 50,000 slices based on my dataset.
var packetPie = dc.pieChart("#packetPie");
packetPie
.height(495)
.width(350)
.radius(180)
.renderLabel(true)
.transitionDuration(1000)
.dimension(mac)
.ordinalColors(['#07453E', '#145C54', '#36847B'])
.group(connectionsGroup);
This works A'OK and I follow up to this point.
(4) Now I want to group by the values given out by the first reducer, i.e I want to combine all of the mac addresses with 1 network connection, 2 network connections and so on as slices.
How would this be done as a dimension of "Network connections"? How can I produce this summarized data which doesn't exist in my source data and is generated from mac?
Or would this require an intermediate function between the first reducer and flattening to combine all of the values from the first reducer?
You don't need to do all of that to get a pie chart of mac addresses.
There are a few faulty understandings in points 1-3, which I guess I'll address first. It looks like you copy and pasted code from the previous question, so I'm not really sure if this helps.
(1) If you have a dimension of mac addresses, reducing it like this won't have any further effect. The original idea was to dimension/group by vendor and then reduce counts for each mac address. This reduction will group by mac address and then further count instances of each mac address within each bin, so it's just an object with one key. It will produce a map of key value pairs like
{key: 'MAC-123', value: {'MAC-123': 12}}
(2) This will flatten the object within the values, dropping the keys and producing just an array of counts
{key: 'MAC-123', value: [12]}
(3) Since the pie chart is expecting simple key/value pairs with the value being a number, it is probably unhappy with getting values like the array [12]. The values are probably coerced to NaN.
(4) Okay, here's the real question, and it's actually not as easy as your previous question. We got off easy with the box plot because the "dimension" (in crossfilter terms, the keys you filter and group on) existed in your data.
Let's forget the false lead in points 1-3 above, and start from first principles.
There is no way to look at an individual row of your data and determine, without looking at anything else, if it belongs to the category "has 1 connection", "has 2 connections", etc. Assuming you want to be able to click on slices in the pie chart and filter all the data, we'll have to find another way to implement that.
But first let's look at how to produce a pie chart of "number of network connections". That's a little bit easier, but as far as I know, it does require a true "double reduce".
If we use the default reduction on the mac dimension, we'll get an array of key/value pairs, where the key is a mac address, and the value is the number of connections for that address:
[
{
"key": "1c:b7:2c:48",
"value": 8
},
{
"key": "1c:b7:be:ef",
"value": 3
},
{
"key": "6c:17:79:03",
"value": 2
},
...
How do we now produce a key/value array where the key is number of connections, and the value is the array of mac addresses for that number of connections?
Sounds like a job for the lesser-known Array.reduce. This function is the likely inspiration for crossfilter's group.reduce(), but it's a bit simpler: it just walks through an array, combining each value with the result of the last. It's great for producing an object from an array:
var value_keys = macPacketGroup.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
Great:
{
"1": [
"b8:1d:ab:d1",
"dc:d9:16:3a",
"dc:d9:16:3b"
],
"2": [
"6c:17:79:03",
"6c:27:79:04",
"b8:1d:aa:d1",
"b8:1d:aa:d2",
"dc:da:16:3d"
],
But we wanted an array of key/value pairs, not an object!
var key_count_value_macs = Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
Great, that looks just like what a "real group" would produce:
[
{
"key": "1",
"value": [
"b8:1d:ab:d1",
"dc:d9:16:3a",
"dc:d9:16:3b"
]
},
{
"key": "2",
"value": [
"6c:17:79:03",
"6c:27:79:04",
"b8:1d:aa:d1",
"b8:1d:aa:d2",
"dc:da:16:3d"
]
},
...
Wrapping all that in a "fake group", which when asked to produce .all(), queries the original group and does the above transformations:
function value_keys_group(group) {
return {
all: function() {
var value_keys = group.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
return Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
}
}
}
Now we can plot the pie chart! The only fancy thing here is that the value accessor should look at the length of the array for each value (instead of assuming the value is just a number):
packetPie
// ...
.group(value_keys_group(macPacketGroup))
.valueAccessor(kv => kv.value.length);
Demo fiddle.
However, clicking on slices won't work. I'll return to that in a minute - just want to hit "save" first!
Part 2: Filtering based on counts
As I remarked at the start, it's not possible to create a crossfilter dimension which will filter based on the count of connections. This is because crossfilter always needs to look at each row and determine, based only on the information in that row, whether it belongs in a group or filter.
If you add another chart at this point and try clicking on a slice, everything in the other charts will disappear. This is because the keys are now counts, and counts are invalid mac addresses, so we're telling it to filter to a key which doesn't exist.
However, we can obviously filter by mac address, and we also know the mac addresses for each count! So this isn't so bad. It just requires a filterHandler.
Although, hmmm, in producing the fake group, we seem to have forgotten value_keys. It's hidden away inside the function, and then let go.
It's a little ugly, but we can fix that:
function value_keys_group(group) {
var saved_value_keys;
return {
all: function() {
var value_keys = group.all().reduce(function(p, kv) {
if(!p[kv.value])
p[kv.value] = [];
p[kv.value].push(kv.key);
return p;
}, {});
saved_value_keys = value_keys;
return Object.keys(value_keys)
.map(k => ({key: k, value: value_keys[k]}));
},
value_keys: function() {
return saved_value_keys;
}
}
}
Now, every time .all() is called (every time the pie chart is drawn), the fake group will stash away the value_keys object. Not a great practice (.value_keys() would return undefined if you called it before .all()), but safe based on the way dc.js works.
With that out of the way, the filterHandler for the pie chart is relatively simple:
packetPie.filterHandler(function(dimension, filters) {
if(filters.length === 0)
dimension.filter(null);
else {
var value_keys = packetPie.group().value_keys();
var all_macs = filters.reduce(
(p, v) => p.concat(value_keys[v]), []);
dimension.filterFunction(k => all_macs.indexOf(k) !== -1);
}
return filters;
});
The interesting line here is another call to Array.reduce. This function is also useful for producing an array from another array, and here we use it just to concatenate all of the values (mac addresses) from all of the selected slices (connection counts).
Now we have a working filter. It doesn't make too much sense to combine it with the box plot from the last question, but the new fiddle demonstrates that filtering based on number of connections does work.
Part 3: what about zeroes?
As commonly comes up, crossfilter considers a bin with value zero to still exist, so we need to "remove the empty bins". However, in this case, we've added a non-standard method to the first fake group, in order to allow filtering. (We could have just used a global there, but globals are messy.)
So, we need to "pass through" the value_keys method:
function remove_empty_bins_pt(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.key !== '0';
});
},
value_keys: function() {
return source_group.value_keys();
}
};
}
packetPie
.group(remove_empty_bins_pt(value_keys_group(macPacketGroup)))
Another oddity here is we are filtering out the key zero, and that's a string here!
Demo fiddle!
Alternately, here's a better solution! Do the bin filtering before passing to value_keys_group, and then we can use the ordinary remove_empty_bins!
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
//return Math.abs(d.value) > 0.00001; // if using floating-point numbers
return d.value !== 0; // if integers only
});
}
};
}
packetPie
.group(value_keys_group(remove_empty_bins(macPacketGroup)))
Yet another demo fiddle!!

Recursively loop through array of objects to build an array of all possible routes with Javascript

I'm trying to write a routing function that will return all possible routes from any two given locations (I'm calling them "spaces"), but I'm stuck on writing the recursive function.
My data will look something like this:
const allSpaces = [
{
id: 0,
name: 'Living Room',
connectedSpaces: [1,2]
},
{
id: 1,
name: 'Hallway A',
connectedSpaces: [0,4]
},
{
id: 2,
name: 'Hallway B',
connectedSpaces: [0,4]
},
{
id: 3,
name: 'Bedroom',
connectedSpaces: [1,2]
}
];
So, calling a getAllRoutes(0,3) method would walk all possible routes and return an array of arrays:
[
[0,1,3],
[0,2,3]
]
Keep in mind that this may not always be as simplistic of a dataset as my example (i.e., Hallway A could have an offshoot that provides an alternate route, or could re-intersect with previously visited spaces).
I'm pretty stumped. I've made several attempts at a recursive function but keep ending up with incomplete lists or infinite loops. Any help would be appreciated!
Visualizing your data
Whenever you find yourself stuck with a problem like this, it helps to think of an easy way to visualize what's happening. To get a feel for the graph you're working with, I wrote a few lines of code to visualize the graph.
Through the visualization, I noticed there's probably a small error in the data. I figured spaces 1 and 2 should be connected to 0 and 3 rather than 0 and 4. I adjusted this in the data, and added an additional space for testing.
If you like, you can check out the visualization by expanding the snippet below.
const allSpaces=[{id:0,name:"Living Room",connectedSpaces:[1,2]},{id:1,name:"Hallway A",connectedSpaces:[0,3,4]},{id:2,name:"Hallway B",connectedSpaces:[0,3]},{id:3,name:"Bedroom",connectedSpaces:[1,2]}, {id:4,name:"Master bedroom",connectedSpaces:[1]}];
const Edge = (() => {
// Do not support two way edges. Cache from and to internally:
const cache = new Map();
const Edge = (from, to) => {
const id = `${Math.min(from, to)}.${Math.max(from, to)}`;
const length = 1;
if (!cache.has(id)) {
cache.set(id, { from, to, id, length });
}
return cache.get(id);
}
return (from => to => Edge(from, to));
})();
const edges = uniques(allSpaces.reduce(
(edges, node) => edges.concat(
node.connectedSpaces.map(Edge(node.id))
), []
));
const Node = ({ id, name }) => ({ id, label: name });
const nodes = allSpaces.map(Node);
const network = new vis.Network(
document.getElementById("cvs"),
{ nodes, edges },
{}
);
function uniques(arr) { return Array.from(new Set(arr).values()); }
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis-network.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
<div id="cvs" style="height: 300px"></div>
Being able to see our data, makes it easier to check if our function works! Now, you've asked to find all paths to get from A to B. Note that this the number of possible paths can very quickly increase as you add more nodes. (e.g., have a look at the Traveling Salesman Problem).
Shortest path?
If you're really looking for the shortest path, you might want to adapt the code below to use Dijkstra's Shortest Path algorithm, as SLaks suggested in their comment.
Brute force approach
But, since the example set is small, and you asked for all routes, let's brute force it:
Define an empty collection of paths
Define a start node
Add its id to the current path
For every node it links to:
Check if it's our destination; if so: return the collection with the current path added
Check if it's already in our path; if so: skip it (we don't want to move in circles)
If it's not in our path nor our destination, add it to the path and move deeper
Or, in code:
const walk = (destination, paths, path, node) => {
// Walking in circles
if (path.includes(node.id)) // <-- expensive, for large paths use a Set
return paths;
// End reached
if (node.id === destination)
return paths.concat([path.concat(node.id)]);
// Take next step recursively
return node.connectedSpaces
.reduce(
(acc, id) => walk(destination, acc, path.concat(node.id), spaceMap.get(id)),
paths
);
}
Here's a running snippet you can use to step through and see what happens:
const allSpaces=[{id:0,name:"Living Room",connectedSpaces:[1,2]},{id:1,name:"Hallway A",connectedSpaces:[0,3,4]},{id:2,name:"Hallway B",connectedSpaces:[0,3]},{id:3,name:"Bedroom",connectedSpaces:[1,2]}, {id:4,name:"Master bedroom",connectedSpaces:[1]}];
const spaceMap = new Map(allSpaces.map(s => [s.id, s]));
const walk = (destination, paths, path, node) => {
// Walking in circles
if (path.includes(node.id)) // <-- expensive, for large paths use a Set
return paths;
// End reached
if (node.id === destination)
return paths.concat([path.concat(node.id)]);
// Take next step recursively
return node.connectedSpaces
.reduce(
(acc, id) => walk(destination, acc, path.concat(node.id), spaceMap.get(id)),
paths
);
}
const calcRoute = (from, to) => {
const routes = walk(to, [], [], spaceMap.get(from));
return `
Found ${routes.length} route(s) to ${spaceMap.get(to).name}
${routes.map(r => r.map(id => spaceMap.get(id).name).join(" -> ")).join("\n")}
`;
}
console.log(calcRoute(0, 3));
console.log(calcRoute(0, 4));

Json and group by solution

I'm trying to select the last messages of an inbox and group them in a list by the topic_id. i want to display the last message of each topic.
the array looks like this:
[{
"id":"5",
"topic_id":"4",
"message_from":"24",
"message":"how do you do?",
"date":"2015-01-13 15:34:59"
},
{
"id":"6",
"topic_id":"1",
"message_from":"33",
"message":"go go go!!",
"date":"2015-01-13 13:35:06"
},
{
"id":"7",
"topic_id":"4",
"message_from":"33",
"message":"Je suis charlie",
"date":"2015-01-14 16:24:46"
},....
is there a solution to do it without a loop?
You can't do this without loops, but you can make this easier by breaking down the sequence of events into smaller functions. You might not like this approach, but it's the cleanest imo. Alternatively you can use a third-party library (underscore, perhaps?) that allows you to run groupings on data.
Basically, get a list of all the topic_ids for all records, loop over that topic_id array and pull out the last record for each and add that to an output array.
// Get a list of all the topic ids - no duplicates
function getTopicIds(arr) {
var out = [];
arr.forEach(function (el) {
if (out.indexOf(el.topic_id) === -1) out.push(el.topic_id);
});
return out;
}
// Given a topic_id, filter the array for only those records
// sort in desc order by id, and return the first record.
// Given that each record has a unique id, and we know that older
// messages will have higher ids, it's easier to sort by id than
// date here
function getLastMsg(id, arr) {
return arr.filter(function (el) {
return el.topic_id === id;
}).sort(function (a, b) { return +b.id - +a.id; })[0];
}
// return a array of the last messages for each topic_id
// in the records array
function getLastMsgs(arr) {
return getTopicIds(arr).map(function (id) {
return getLastMsg(id, arr);
});
}
var result = getLastMsgs(arr);
DEMO

Ember store adding attributes incorrectly

I'm using the latest version of ember-cli, ember-data, ember-localstorage-adapter, and ember.
I have a Node object which has a parent and children. Since I had issues with creating multiple relationships with the same type of object, I decided to store the parentID in a string, and the childIDs in an array of strings. However, when I create a new Node and try to add the new Node's to the parents array of IDs, the ID ends up being added to the correct parent, but also other parents.
level 1 0
/ \
level 2 1 2
| |
level 3 3 4
In a structure like this, 0, 1, and 2 all have correct child and parent IDs. However, after adding 3 and 4, node 1 and node 2's childIDs are [3, 4], instead of [3], [4] respectively.
The Array attribute:
var ArrayTransform = DS.Transform.extend({
serialize: function(value) {
if (!value) {
return [];
}
return value;
},
deserialize: function(value) {
if (!value) {
return [];
}
return value;
}
});
The insertNode code:
insert: function(elem) {
var i,
_store = elem.node.store,
newNodeJSON = elem.node.serialize();
newNodeJSON.childIds = [];
newNodeJSON.level = getNextLevel();
_store.filter('node', function(node) {
return node.get('level') === newnodeJSON.level-1;
}).then(function(prevLevelNodes) {
// if no other nodes yet
if (prevLevelNodes.toArray().length === 0) {
makeNewNode(_store, newNodeJSON, elem.node);
}
// else, generates however many nodes that are in the previous level
else {
prevLevelNodes.toArray().forEach(function(node, idx) {
newNodeJSON.parentId = node.get('id');
makeNewNode(_store, newNodeJSON, elem.node);
});
}
});
}
var makeNewNode = function(_store, newNodeJSON, node) {
console.log(newNodeJSON.parentId); // returns correct value
var newNode = _store.createRecord('node', newNodeJSON);
newNode.save();
var newNodeId = newNode.get('id');
if (newNode.get('parentId')) {
_store.find('node', newNode.get('parentId')).then(function(n) {
var cids = n.get('childIds');
console.log(newNodeId); // returns expected value
console.log(cids); // **DOESN'T RETURN AN EMPTY ARRAY**: returns array with [3,4]
cids.push(newNodeId);
console.log(n.get('childIds')); // returns array with [3,4]
n.save();
});
}
To top this off, this error happens 90% of the time, but 10% of the time it performs as expected. This seems to suggest that there's some sort of race condition, but I'm not sure where that would even be. Some places that I feel like might be causing issues: the ember-cli compilation, passing the entire _store in when making a new node, ember-data being weird, ember-localstorage-adapter being funky... no clue.
For anyone else who may have this problem in the future: the problem lies in two things.
In ArrayTransform, typically I am returning the value sans modification.
In my insert code, I'm passing the same JSON that I defined at the top of the function to makeNewNode.
This JSON contains a reference to a single childIds array; therefore, each new node that gets created uses this same reference for its childIds. Although this doesn't quite explain why the cids array wasn't empty before the push executed (perhaps this is some sort of compiler oddity or console printing lag), it explains why these both Level 3 children were in both Level 2 parents' childIds array.
tl;dr: pass by value vs pass by reference error

Categories

Resources