thi-ng / umbrella

⛱ Broadly scoped ecosystem & mono-repository of 199 TypeScript projects (and ~180 examples) for general purpose, functional, data driven development
https://thi.ng
Apache License 2.0
3.41k stars 151 forks source link

[associative] Unexpected behavior when setting new values for a SortedMap created with initial values #375

Closed alexlurvey closed 1 year ago

alexlurvey commented 1 year ago

I noticed that calling 'set' on a SortedMap will have different behavior depending on whether or not the map was created with initial entries. For maps with initial entries, 'set' will sometimes apply the value to another key. It doesn't always do this, but you should notice different results in the example sandbox after running it a few times.

https://codesandbox.io/s/condescending-bell-vsbpyq?file=/src/index.ts

postspectacular commented 1 year ago

Thank you for spotting this @alexlurvey (also the sandbox is super helpful!) It's been a few years since I wrote that Skip-List impl, but will take a look asap!

postspectacular commented 1 year ago

@alexlurvey - I just finished a major rewrite of the .set() & .delete() methods, using an easier internal node structure and also easier to follow/debug now... Did a bunch of testing in the REPL, and will add some more tests. Right now it all seems to work reliably (also your example). Will release ASAP!

alexlurvey commented 1 year ago

@postspectacular Thank you. I appreciate all the effort. Everything checks out on my end after working with the new implementation. And, agreed, the new code is much easier to follow - I've got a decent grasp on how skip lists work now :)

postspectacular commented 1 year ago

Great! Meanwhile I also added a few more comments & released everything - thanks again! :)

https://github.com/thi-ng/umbrella/blob/develop/packages/associative/CHANGELOG.md

alexlurvey commented 1 year ago

@postspectacular I think I did find something else that's not quite right. Calling values() or entries() after updating some keys won't always produce the latest values: https://codesandbox.io/s/xenodochial-cache-22l2z2?file=/src/index.ts

postspectacular commented 1 year ago

@alexlurvey Hmm... so sorry, just proves that all the tests are still not sufficient... I will take another look ASAP, and I might after all implement the graphviz export to visualize the node structures (something I almost did in that last round, to help me debug)

postspectacular commented 1 year ago

I think I've fixed it (for real)! As you can see from the attached diagram, an update of an existing key currently only updated the first matching node in any of the "exress lane" skip lists, but not propagate the value to the lower levels.

Screen Shot 2023-02-07 at 14 18 11

So in this particular (random) configuration, if I update the "one" or "three" keys, it would only update the 2 bright green nodes (here with values 10 & 30), but not the ones connected via down links (e.g. the pale green nodes, which still have value 1). Updating the "two" key would work fine here, since this one is only stored on the lowest level...

I've already pushed the fix (see commit above), would be grateful if you could also test some more before I release...

alexlurvey commented 1 year ago

@postspectacular thanks for the explanation & diagram. I'll do some more through testing later today and report back. I'll also plan on filling out those test with some of my use cases.

postspectacular commented 1 year ago

@alexlurvey Noice & look forward hearing back! :)

alexlurvey commented 1 year ago

@postspectacular I think we're golden. I wired it up to the test fixture I'm working on (a list of UI inputs that can add or remove items depending on the selected value) and didn't experience any missing values.

alexlurvey commented 1 year ago

@postspectacular Hello again 😬 I think I may have found another item to fix after writing some more unit tests. I occasionally see TypeError: Cannot read properties of undefined (reading 'up') when calling 'set'. That's coming from this portion of the code (line 229):

// find nearest predecessor with up link
while (!node!.up) node = node!.prev;
node = node!.up;

When I change that to this, I'm able to run my test suite 100 times without failure:

while (node!.prev && !node!.up) {
    if (node!.prev) node = node!.prev;
}
node = node?.up ?? node;

I was trying to get branch setup to reproduce, but when running this in packages/associative I eventually get an "Unable to compile TypeScript" error with some issues in the sorted-map TS file. Probably a local issue though.

for i in {1..100}; do yarn test; if [[ "$?" != 0 ]]; then echo "Failed after $i attempts" && break; fi; done

postspectacular commented 1 year ago

@alexlurvey - mon dieu! :) Thank you for this - I think that insertion logic is fine as is since each lane should always have at least its heads vertically connected. Also I cannot reproduce the issue when only every calling .set(). It only seems to appear when you also delete keys, which means there's something wrong with the .delete() implementation, i.e. it's somewhere corrupting lanes to have no more head nodes... will be looking into it later today!

postspectacular commented 1 year ago

@alexlurvey one more round (please) & also added another fuzz test myself...

alexlurvey commented 1 year ago

@postspectacular this is working fine against my tests. I just ran them all 200 times - all green. Thanks!

postspectacular commented 1 year ago

Yeah, thank you for the speedy reply - that means I can push the new release(s) in the next 30 mins! 🚀

postspectacular commented 1 year ago

@alexlurvey Just in case you haven't seen it yet, I released the new version ~4h ago: https://mastodon.thi.ng/@toxi/109841142601682573

Thank you again for helping with fixing this! Gonna need that class soon myself again!