feat: Expose hasStagedChanges/hasStagedChangesChanged on IContainerRuntimeBase#27648
feat: Expose hasStagedChanges/hasStagedChangesChanged on IContainerRuntimeBase#27648dannimad wants to merge 9 commits into
Conversation
…gesChanged Adds robust test coverage for the hasStagedChanges/hasStagedChangesChanged feature, mirroring the existing isDirty/dirty test patterns: - PendingStateManager unit tests for hasStagedChanges() - ContainerRuntime baseline "Staged changes flag" tests across Attached/Attaching/Detached states - ContainerRuntime staging-mode lifecycle tests (commitChanges/discardChanges) - ContainerRuntime hasStagedChangesChanged event tests - End-to-end Document Staged Changes lifecycle tests in local-server-tests Also fixes a real bug found while testing: hasStagedChanges/ hasStagedChangesChanged were not refreshed after flush() alone, so the cached value could remain stale even with staged ops pending in the PendingStateManager. Now updateHasStagedChangesState() is called at the end of ContainerRuntime.flush(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Hi! Thank you for opening this PR. Want me to review it? Based on the diff (1077 lines, 17 files), I've queued these reviewers:
How this works
|
Renames the hasStagedChanges changeset from a manually-chosen descriptive filename to a randomly-generated one produced by `flub changeset add --empty`, matching the normal changeset creation flow used elsewhere in the repo. Content is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…imeBase Per review feedback, StagingMode-related concepts should stay on IContainerRuntimeBase for now rather than being exposed on IContainer directly. This commit: - Removes hasStagedChanges/hasStagedChangesChanged from IContainer, IContainerEvents, and IContainerContext.updateStagedChangesState (container-definitions, container-loader). - Moves hasStagedChanges/hasStagedChangesChanged from IContainerRuntime (container-runtime-definitions) to IContainerRuntimeBase (runtime-definitions), alongside the existing enterStagingMode/ inStagingMode/stagingModeChanged surface. - Updates ContainerRuntime's wiring to no longer notify the container layer of staged-changes state (only dirty state remains plumbed through to the container). - Updates existing tests and mocks accordingly, and rewrites documentStagedChanges.spec.ts to assert only at the ContainerRuntime level. - Regenerates API reports and type tests for the affected packages, and updates the changeset to reflect the new API surface location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
markfields
left a comment
There was a problem hiding this comment.
Looks pretty good, but I think there's a mismatch with what expected behavior probably is for a consumer - they'll expect hasStagedChanges to be true as soon as the change happens, regardless of when the batch flushes from the Outbox.
| } | ||
| } | ||
|
|
||
| this.updateHasStagedChangesState(); |
There was a problem hiding this comment.
This feels excessive — updateDocumentDirtyState is called from ~6 places (process, resubmit, connect, rollback, etc.) and most of those can't change staged-changes state. The only transition this catches that the other two call sites (flush() line 3728, commitChanges line 3922) don't is discardChanges (line 3910 calls updateDocumentDirtyState but not updateHasStagedChangesState).
Consider dropping this from updateDocumentDirtyState and adding an explicit this.updateHasStagedChangesState() call in the discardChanges lambda (after the updateDocumentDirtyState() at line 3910). More surgical, avoids running the check on every unrelated dirty-state update.
There was a problem hiding this comment.
Agreed — confirmed 6 call sites, only discardChanges/commitChanges/flush/submitMessage can actually affect staged state. Removed the blanket call from updateDocumentDirtyState and added explicit this.updateHasStagedChangesState() calls only where staged state can truly change: discardChanges, and submitMessage (new — see next item).
| // orderSequentially with EnableRollback calls enterStagingModeCore(silent=true) internally, | ||
| // which suppresses stagingModeChanged. hasStagedChangesChanged is driven by | ||
| // updateDocumentDirtyState/updateHasStagedChangesState instead, which are not gated by | ||
| // the `silent` flag, so it CAN fire in this scenario. |
There was a problem hiding this comment.
Why is this the preferred design? Or are we ambivalent and just documenting what it does (which is the simplest approach)
There was a problem hiding this comment.
Just documenting. I think this could be a scenario where we could ask clarification and fix in a follow up
| // Use a low threshold so staged ops are auto-flushed to the PendingStateManager (which is what | ||
| // hasStagedChanges checks) right away instead of waiting for the default (1000 ops) batch size. | ||
| runtimeOptions: { stagingModeAutoFlushThreshold: 1 }, |
There was a problem hiding this comment.
Ok this raises an interesting question. I had forgotten about this autoflush feature. I wonder if hasStagedChanges should also return true if the outbox is nonempty and we're in staging mode. I think the difference between outbox and PSM is irrelevant to a consumer using Staging Mode.
There was a problem hiding this comment.
This was a real bug: while in Staging Mode, a submitted op sits in the Outbox until the next flush, and during that window hasStagedChanges incorrectly reported false. Fixed computeCurrentHasStagedChanges() to also check this.inStagingMode && this.outbox.containsUserChanges(). Updated submitMessage to call updateHasStagedChangesState() explicitly, and updated/added tests in both containerRuntime.spec.ts and documentStagedChanges.spec.ts to assert the value is true immediately on submit, before flush.
| }); | ||
| }); | ||
|
|
||
| describe("hasStagedChangesChanged event", () => { |
There was a problem hiding this comment.
Consider adding a unit test here that enters staging mode, submits no ops, then exits (both commit and discard paths) — asserting hasStagedChangesChanged is never emitted (event count stays 0) and hasStagedChanges remains false throughout. The e2e test checks the property at line 171 but doesn't assert the event isn't fired, and this is the right place for that kind of negative assertion as a unit test.
There was a problem hiding this comment.
Missing test coverage (3 separate suggestions)
Added all three:
Unit tests for entering/discarding and entering/committing staging mode with no ops — assert hasStagedChangesChanged never fires and hasStagedChanges stays false.
documentStagedChanges.spec.ts: new test asserting the commit path — isDirty stays true (in-flight/unacked) right after commitChanges() while hasStagedChanges flips to false.
documentStagedChanges.spec.ts: new test loading a second, freshly-loaded container from the same document while the first has uncommitted staged changes — asserts the fresh container reports hasStagedChanges: false (added a loadContainer helper using loadExistingContainer for this).
|
🔗 No broken links found! ✅ Your attention to detail is admirable. linkcheck output |
Bundle size comparisonBase commit: Notable changes
Per-bundle deltas
|
| * (see {@link @fluidframework/runtime-definitions#IContainerRuntimeBaseInternal.enterStagingMode}) | ||
| * that have not yet been discarded or committed. | ||
| * | ||
| * @remarks This is distinct from {@link ContainerRuntime.isDirty}: a container may be dirty due to |
There was a problem hiding this comment.
The "and vice versa" should be kept here after all! Or explain the case specifically - The new unit test (hasStagedChanges is true for a non-dirtyable op sitting in the Outbox while staging) explicitly demonstrates that hasStagedChanges can be true while isDirty is false — that's the "vice versa" case.
| * not yet been discarded or committed. | ||
| * | ||
| * @remarks This is distinct from {@link @fluidframework/container-runtime-definitions#IContainerRuntime.isDirty}: a container may be dirty due to | ||
| * ordinary unacknowledged local changes without having any staged changes. |
There was a problem hiding this comment.
Same here — "and vice versa" should be kept. A non-dirtyable op submitted during staging mode results in hasStagedChanges=true but isDirty=false (as the new unit test demonstrates).
|
|
||
| containerRuntime.on("hasStagedChangesChanged", (hasStagedChanges) => { | ||
| events.push(hasStagedChanges); | ||
| hasStagedChangesAtEventTime.push(containerRuntime.hasStagedChanges); |
There was a problem hiding this comment.
nit: just assert here that containerRuntime.hasStagedChanges === hasStagedChanges? And remove hasStagedChangesAtEventTime
| ); | ||
| }); | ||
|
|
||
| it("returns true if a staged message is an empty batch (runtimeOp undefined)", () => { |
There was a problem hiding this comment.
Side note - I don't think this is possible anyway.
| const dirty: boolean = this.computeCurrentDirtyState(); | ||
|
|
||
| if (this.lastEmittedDirty === dirty) { | ||
| if (this.lastEmittedDirty !== dirty) { |
There was a problem hiding this comment.
Why is this being inverted? Especially considering that updateHasStagedChangesState follows the other form!
It's a nit, but I'd revert this to reduce the diff and keep them symmetrical.
|
|
||
| stageControls.commitChanges(); | ||
|
|
||
| checkStagedChangesState("after commitChanges", false, 1, 1); |
There was a problem hiding this comment.
Also assert isDirty is true here then false after ensureSynchronized?
[edit] - Oh, there is that test below. Are they redundant otherwise?
CraigMacomber
left a comment
There was a problem hiding this comment.
The changeset seems fine from a docs perspective.
Description
IContainerRuntimeBasenow exposeshasStagedChanges, a boolean indicating whether there are any changes submitted while in Staging Mode (viaenterStagingMode) that have not yet been discarded or committed. A newhasStagedChangesChangedevent is emitted whenever this value changes.This is distinct from
isDirty/dirty: a container runtime can be dirty due to ordinary unacknowledged local changes without having any staged changes, and vice versa. Per review feedback, StagingMode-related concepts stay onIContainerRuntimeBasefor now rather than being exposed onIContainer.Test coverage mirrors the existing
isDirty/dirtytest suites:PendingStateManagerunit tests forhasStagedChanges()ContainerRuntimebaseline flag tests across Attached/Attaching/Detached statesContainerRuntimestaging-mode lifecycle tests (commitChanges/discardChanges)ContainerRuntimehasStagedChangesChangedevent testslocal-server-tests(documentStagedChanges.spec.ts)While adding tests, found and fixed a real bug:
hasStagedChanges/hasStagedChangesChangedwere not refreshed afterflush()alone, so the cached value could remain stale (false) even with staged ops pending in thePendingStateManager. Fixed by callingupdateHasStagedChangesState()at the end ofContainerRuntime.flush().Reviewer Guidance
The review process is outlined on this wiki page.
flush()fix is the only behavioral change outside of tests — please double check the reasoning around staleness inupdateHasStagedChangesState().