-
Notifications
You must be signed in to change notification settings - Fork 46
feat(dev-tools): operation history #598
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📊 Benchmark Analysis Report✅ No significant performance changes detected Threshold: 5% change Updated: 2025-11-20T11:55:19.841Z |
6729b8a to
932bf32
Compare
bed2872 to
4f41b34
Compare
53bfa9a to
4e1e27e
Compare
vladar
left a comment
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.
Amazing stuff. Going to be a game changer for debugging state issues! But before releasing it, we need a couple of tweaks in the core ForestRun code.
| @@ -0,0 +1,24 @@ | |||
| import type { HistoryEntry } from "../forest/types"; | |||
|
|
|||
| export class HistoryArray { | |||
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.
This could be a generic utility called CircularBuffer. The idea is that jsutils folder contains lower-level things without ForestRun-specific semantics, i.e. applicable in any JS project.
When changing to CircularBuffer, HistoryEntry should become a generic TItem on a class of course
Also make it iterable for convenient iteration and Array.from() compatibility.
| export class HistoryArray { | |
| export class HistoryArray implements Iterable<HistoryEntry> { | |
| [Symbol.iterator]() { | |
| // implementation | |
| } |
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.
Have change it to Circular buffer: https://github.com/microsoft/graphitation/pull/598/files#diff-38bfd45bc1835790dfc54f648d8091638169536f59ddb5e000c0a95a75aa0c15
| if (shouldPushOptimisticHistory(targetForest)) { | ||
| results.outputTree.history.push( |
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.
shouldPushOptimisticHistory should return false when history is disabled, which is not the case now. Today this will always add history entry, even if it is disabled, which is not what we want, right?
Oh I see now, history.push ignores undefined values and createOptimisticHistoryEntry returns undefined when history is disabled. This is somewhat counter-intuitive when reading this code. Let's please change it:
- make
history.push()acceptHistoryEntry(do not accept undefined) - make
shouldPushOptimisticHistoryreturnfalsewhen the flag is off.
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.
Change the logic the handling the condition in caller.
| enableRichHistory: config?.enableRichHistory ?? false, | ||
| defaultHistorySize: config?.defaultHistorySize ?? 0, |
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.
As a sanity check I suggest you to run the full test suite with history enabled before merging (manually). It may uncover some hidden quirks.
Ideally, also run with other flags enabled/disabled, especially optimizeFragmentReads.
| if (dirtyFields?.length) { | ||
| context.changes.set(base, dirtyFields); | ||
| } |
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.
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.
Change the logic and pushing always changes. I have change the structure of those changes and since they are not used anywhere it should be safe.
| export type ChangedChunksMap = Map<ObjectChunk, FieldInfo[]> & | ||
| Map<CompositeListChunk, null>; | ||
| export type ChangedChunksMap = Map< | ||
| ObjectChunk | CompositeListChunk, |
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 don't see in the actual code that the key could be CompositeListChunk. Am I missing something?
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.
Changed to
export type ChangedChunksMap = Map<
CompositeListChunk,
CompositeListDifferenceEntry
> &
Map<ObjectChunk, (FillerEntry | ReplacementEntry)[]>;But it is quite tricky to iterate this intersection type. I did not find better way than typecast it when iteration. So there is also a tuple but it is not directly connected to the map.
export type ChangedChunksTuple =
| [CompositeListChunk, CompositeListDifferenceEntry]
| [ObjectChunk, (FillerEntry | ReplacementEntry)[]];| if (dirty) { | ||
| context.changes.set(base, null); | ||
| } |
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.
Same, we should always capture actual changes, even if history is disabled (we just don't add changes to history if its disabled). Maybe childChanges is a replacement you meant. But then please double-check that this context.changes is not used anywhere, especially in the context of optimizeFragmentReads flag.
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.
Changed, now I am capturing always changes

No description provided.