-
Notifications
You must be signed in to change notification settings - Fork 364
Switch internal directory representation to use maps #1169
Conversation
The same error probably occurs with files that're named EDIT: Confirmed. |
Really? I'm pretty sure I tested out a |
lib/directory-view.coffee
Outdated
view.element.remove() | ||
subscription.dispose() | ||
break | ||
# TODO: When this file is converted to JS, convert this forEach loop | ||
# to a for...of loop and add a break here for performance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option is to avoid the "problem" by using Array.prototype.some
instead;
removedEntries.some (removedEntry, removedName) ->
return true unless entry is removedEntry
view.element.remove()
subscription.dispose()
This code will behave essentially exactly like the old version — it will iterate until the desired item is encountered, then short-circuit the iteration. You could of course also achieve this by using Array.prototype.every
, but the version above more closely mimics the old version I think (at least in terms of lines of code, and overall legibility).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind... just realized you're working with Map
and not Array
— d'oh!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, it's annoying because Coffeescript's for...of
is JS's for...in
, and of course its for...in
is just a fancy for loop and not for...of
. So there's no way to loop Map entries. Oh well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you already checked that using Map
s does not break consumers of this API, I wonder if we could change the parameters to make this code path more efficient. In particular, what about exposing a Set
of entries that were removed so that we can retrieve it in O(1)
here? Like:
if (removedEntries.has(entry)) {
// cleanup
}
An alternative might be to rely on the name and avoid changing the API altogether:
if (removedEntries.has(entry.name)) {
// cleanup
}
Although I am slightly concerned about the order of events when a rename occurs if we decide to go choose the second option.
What do you think? 💭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you expand on your concerns about the event order with the second option?
Yeah, sorry if that comment was a little obscure. My primary concern is that in the same tick of the loop the following could happen:
- An entry with name
x
is added. - A previous entry with name
x
is deleted.
I am not sure this would be possible in the current model, but if we receive change events asynchronously I worry that the add and the delete event could get grouped together and cause the wrong entry (or both, in the example above) to be deleted.
If that's a circumstance that we are sure we will never encounter, then I think we could go ahead and implement 2). Otherwise, relying on reference equality seems to be safer.
@Alhadis are you trying with my PR? The |
Oh! Whoops, my bad. Yeah, your branch handles files as well. Never mind. 👼 |
Just reminding myself which part of the serialization I need to work on:
|
lib/directory.coffee
Outdated
@@ -26,6 +26,16 @@ class Directory | |||
@isRoot ?= false | |||
@expansionState ?= {} | |||
@expansionState.isExpanded ?= false | |||
|
|||
# TODO: This can be removed after a sufficient amount | |||
# of time has passed since @expansionState.entries |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Waiting until after #1146 should be enough, too. 😜
So it looks like only Tree View forks are using |
@50Wliu Just an FYI, there were indeed regressions with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left one minor comment, but otherwise this looks good to me.
Nice work, @50Wliu! ⚡️
lib/directory-view.coffee
Outdated
view.element.remove() | ||
subscription.dispose() | ||
break | ||
# TODO: When this file is converted to JS, convert this forEach loop | ||
# to a for...of loop and add a break here for performance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you already checked that using Map
s does not break consumers of this API, I wonder if we could change the parameters to make this code path more efficient. In particular, what about exposing a Set
of entries that were removed so that we can retrieve it in O(1)
here? Like:
if (removedEntries.has(entry)) {
// cleanup
}
An alternative might be to rely on the name and avoid changing the API altogether:
if (removedEntries.has(entry.name)) {
// cleanup
}
Although I am slightly concerned about the order of events when a rename occurs if we decide to go choose the second option.
What do you think? 💭
One more question, related to @Alhadis' comment:
Are we worried at all about breaking other packages by shipping this pull-request? |
@as-cii I don't imagine there'd be a risk of runtime breakage. They were POJOs in the first place, which means any package using them would probably only be looping through their properties with a However, this might lead to subtle regressions in packages which expect to find filenames in the form of enumerated key/value pairs (as |
@as-cii could you expand on your concerns about the event order with the second option? |
@as-cii I have implemented the first option, just to be safe. |
Requirements
Description of the Change
This PR switches the internal representations of directory entries to use
Map
s instead ofObject
s. While they are very similar, there is one big disadvantage to using Objects - Objects have internal and inherited properties/keys. Since setting object keys sets the key directly, this means that certain directory names can conflict and override the existing Object properties. A notable example of this is__proto__
. Naming a directory__proto__
causes Tree View to override the base Object prototype, which leads to Very Bad Things(tm). Maps do not have this limitation.Alternate Designs
Switching to
Map
was the first thing I considered, and it worked. Alternatives would be to refuse to load folders that contained__proto__
entries, to prevent__proto__
entries from being added to the Directory representation, or to use a different data structure. I chose Maps due to the reasons outlined above and because MDN states that Maps should be used if possible when there is the potential for unknown keys.Benefits
The name of a directory should no longer render Atom unusable.
Possible Drawbacks
This changes the output of
onDidRemoveEntries
from an Object to a Map. I'm unaware if that's being used anywhere outside of Tree View (see below). In addition, the serialized expansion state has also changed from an Object to a Map, meaning that extra conversion code will need to be added for a smooth transition.Applicable Issues
Fixes #1079
/cc @Alhadis to make sure that this doesn't affect file-icons (in my testing it didn't)
/cc @Daniel15
TODO: