Skip to content

fix: skip identity changesets during timeslider playback#7438

Open
JohnMcLear wants to merge 1 commit intoether:developfrom
JohnMcLear:fix/timeslider-identity-changeset-5214
Open

fix: skip identity changesets during timeslider playback#7438
JohnMcLear wants to merge 1 commit intoether:developfrom
JohnMcLear:fix/timeslider-identity-changeset-5214

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

Fix in broadcast.ts: All three applyChangeset call sites now check for identity changesets using the existing isIdentity() helper from Changeset.ts and skip them.

Root Cause

When a pad's revision history contains an identity changeset (Z:N>0$, meaning no actual change), the timeslider playback tried to apply it via mutateAttributionLines and mutateTextLines. These functions can error or corrupt state when given a changeset with no operations.

Identity changesets can appear in the revision history when:

  • An import replaces all content (creating an empty-then-repopulate sequence)
  • A save occurs with no pending changes
  • compose() of multiple revisions produces a net-zero change

The existing truthiness check (if (changeset)) didn't catch identity changesets because they're non-empty strings like "Z:1>0$".

Test plan

  • Frontend: 2 new Playwright tests — timeslider playback with delete+retype history, and scrubbing through revisions (both pass)
  • Backend: messages.ts tests pass (10/10)
  • Manual: import the .etherpad data from the issue, open timeslider, click play → should play through without errors

Fixes #5214

🤖 Generated with Claude Code

When a pad's revision history contains an identity changeset (Z:N>0$,
representing no actual change), the timeslider playback would crash or
break because broadcast.ts tried to apply it via mutateAttributionLines
and mutateTextLines.

Now all three applyChangeset call sites in broadcast.ts check for
identity changesets using the existing isIdentity() helper and skip
them. This also prevents errors when compose() produces an identity
changeset from multiple revisions that cancel each other out.

Fixes: ether#5214

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Skip identity changesets during timeslider playback

🐞 Bug fix 🧪 Tests

Grey Divider

Walkthroughs

Description
• Skip identity changesets during timeslider playback to prevent crashes
• Add isIdentity() checks at all three applyChangeset call sites
• Import isIdentity helper from Changeset.ts module
• Add two Playwright regression tests for timeslider with identity changesets
Diagram
flowchart LR
  A["Timeslider playback"] --> B["Check if changeset is identity"]
  B -->|Identity| C["Skip applyChangeset"]
  B -->|Real change| D["Apply changeset normally"]
  C --> E["No crash or corruption"]
  D --> E
Loading

Grey Divider

File Changes

1. src/static/js/broadcast.ts 🐞 Bug fix +12/-4

Add identity changeset checks to broadcast playback

• Import isIdentity helper function from Changeset module
• Add isIdentity() checks before all three applyChangeset call sites
• Skip applying identity changesets that represent no actual change
• Add explanatory comment referencing issue #5214

src/static/js/broadcast.ts


2. src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts 🧪 Tests +93/-0

Add timeslider identity changeset regression tests

• Add regression test for timeslider playback with multiple revisions
• Test delete-and-retype scenario that triggers identity changesets
• Verify no error gritter popups appear during playback
• Add test for scrubbing through all revisions without errors

src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 2, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Protocol-specific URLs in tests 📘 Rule violation ⚙ Maintainability
Description
New code introduces protocol-specific URLs (for localhost navigation and an issue link), violating
the requirement to use protocol-independent URLs. This can reduce portability across environments
and breaks the documented coding style rule.
Code

src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts[36]

+    await page.goto(`http://localhost:9001/p/${padId}/timeslider`);
Evidence
PR Compliance ID 8 requires protocol-independent URLs, but the added Playwright spec navigates using
http://localhost:9001/... and the added broadcast comment includes an https:// link.

src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts[36-36]
src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts[78-82]
src/static/js/broadcast.ts[281-283]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Newly added code uses protocol-specific URLs (e.g., `http://...` and `https://...`) where the coding style requires protocol-independent URLs.

## Issue Context
This is explicitly disallowed by the PR compliance checklist (Coding Style requires protocol-independent URLs such as `//example.com`).

## Fix Focus Areas
- src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts[36-36]
- src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts[78-82]
- src/static/js/broadcast.ts[281-283]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Identity skip stalls revision 🐞 Bug ≡ Correctness
Description
broadcast.ts now skips applyChangeset() for identity changesets, but applyChangeset() is also what
advances padContents.currentRevision/currentTime and (when following latest) moves the slider;
skipping it can leave the timeslider desynced/stuck on an older revision. This can break
live-following after an identity revision arrives and can break scrubbing because goToRevision()
continues computing paths from a stale padContents.currentRevision.
Code

src/static/js/broadcast.ts[R204-206]

+    if (broadcasting && !isIdentity(changesetForward)) {
+      applyChangeset(changesetForward, revision + 1, false, timeDelta);
+    }
Evidence
applyChangeset() updates slider position (when not prevented) and is the only place that updates
padContents.currentRevision and padContents.currentTime; the PR’s new !isIdentity(...) guards
prevent applyChangeset() from running, so those state updates do not happen for identity revisions.
goToRevision() computes paths from padContents.currentRevision and is wired to slider movement
callbacks, so leaving currentRevision stale risks wrong path computation and UI desync.

src/static/js/broadcast.ts[135-196]
src/static/js/broadcast.ts[198-207]
src/static/js/broadcast.ts[265-315]
src/static/js/broadcast.ts[465-476]
src/static/js/Changeset.ts[1161-1164]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new identity-changeset guards skip `applyChangeset()`, but that function also advances `padContents.currentRevision/currentTime` and (in live-follow mode) moves the slider. For identity revisions, you still need to advance revision/time (and potentially slider) without calling `mutateAttributionLines()`/`mutateTextLines()`.

### Issue Context
- `applyChangeset()` is responsible for updating `padContents.currentRevision`, `padContents.currentTime`, and (when `preventSliderMovement` is false) `BroadcastSlider.setSliderPosition(revision)`.
- The PR added `!isIdentity(...)` checks at multiple call sites, so identity revisions currently do not advance state.

### Fix Focus Areas
- src/static/js/broadcast.ts[135-196]
- src/static/js/broadcast.ts[198-207]
- src/static/js/broadcast.ts[265-315]

### Implementation direction (one of)
1) Move the identity handling into `applyChangeset()`:
  - At the top of `applyChangeset()`, if `isIdentity(changeset)` then:
    - If `!preventSliderMovement`, do the slider movement bookkeeping (`goToRevisionIfEnabledCount++` + `BroadcastSlider.setSliderPosition(revision)`).
    - Update `padContents.currentRevision = revision` and `padContents.currentTime += timeDelta`.
    - Call `updateTimer()` and update authors UI as needed.
    - Return early (do **not** call the mutators).
  - Then remove (or relax) the `!isIdentity(...)` guards at the call sites.

2) Alternatively, add a small helper like `advanceRevision(revision, timeDelta, preventSliderMovement)` and call it in the `isIdentity` branches at each call site.

Either way, ensure live-following continues across identity revisions and `goToRevision()` ends with `padContents.currentRevision === path.rev` even if the composed changeset is identity.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Unused identity import 🐞 Bug ⚙ Maintainability
Description
broadcast.ts imports identity from Changeset.ts but never uses it, which is likely to be flagged
by linting and fail the repo’s eslint-based lint script. This also increases confusion because the
fix only uses isIdentity().
Code

src/static/js/broadcast.ts[29]

+import {compose, deserializeOps, identity, inverse, isIdentity, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, splitAttributionLines, splitTextLines, unpack} from './Changeset';
Evidence
The identity symbol is imported in broadcast.ts but is not referenced elsewhere in the file, and
the project lint script runs eslint ., which commonly reports unused imports.

src/static/js/broadcast.ts[26-33]
src/package.json[134-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`identity` is imported from `./Changeset` in `broadcast.ts` but never used.

### Issue Context
The project runs `eslint .` via the `lint` script, and unused imports are typically reported.

### Fix Focus Areas
- src/static/js/broadcast.ts[26-33]

### Suggested fix
Remove `identity` from the import list unless you end up using it as part of the identity-changeset handling refactor.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

await page.waitForTimeout(1000);

// Navigate to timeslider
await page.goto(`http://localhost:9001/p/${padId}/timeslider`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Protocol-specific urls in tests 📘 Rule violation ⚙ Maintainability

New code introduces protocol-specific URLs (for localhost navigation and an issue link), violating
the requirement to use protocol-independent URLs. This can reduce portability across environments
and breaks the documented coding style rule.
Agent Prompt
## Issue description
Newly added code uses protocol-specific URLs (e.g., `http://...` and `https://...`) where the coding style requires protocol-independent URLs.

## Issue Context
This is explicitly disallowed by the PR compliance checklist (Coding Style requires protocol-independent URLs such as `//example.com`).

## Fix Focus Areas
- src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts[36-36]
- src/tests/frontend-new/specs/timeslider_identity_changeset.spec.ts[78-82]
- src/static/js/broadcast.ts[281-283]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +204 to +206
if (broadcasting && !isIdentity(changesetForward)) {
applyChangeset(changesetForward, revision + 1, false, timeDelta);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Identity skip stalls revision 🐞 Bug ≡ Correctness

broadcast.ts now skips applyChangeset() for identity changesets, but applyChangeset() is also what
advances padContents.currentRevision/currentTime and (when following latest) moves the slider;
skipping it can leave the timeslider desynced/stuck on an older revision. This can break
live-following after an identity revision arrives and can break scrubbing because goToRevision()
continues computing paths from a stale padContents.currentRevision.
Agent Prompt
### Issue description
The new identity-changeset guards skip `applyChangeset()`, but that function also advances `padContents.currentRevision/currentTime` and (in live-follow mode) moves the slider. For identity revisions, you still need to advance revision/time (and potentially slider) without calling `mutateAttributionLines()`/`mutateTextLines()`.

### Issue Context
- `applyChangeset()` is responsible for updating `padContents.currentRevision`, `padContents.currentTime`, and (when `preventSliderMovement` is false) `BroadcastSlider.setSliderPosition(revision)`.
- The PR added `!isIdentity(...)` checks at multiple call sites, so identity revisions currently do not advance state.

### Fix Focus Areas
- src/static/js/broadcast.ts[135-196]
- src/static/js/broadcast.ts[198-207]
- src/static/js/broadcast.ts[265-315]

### Implementation direction (one of)
1) Move the identity handling into `applyChangeset()`:
   - At the top of `applyChangeset()`, if `isIdentity(changeset)` then:
     - If `!preventSliderMovement`, do the slider movement bookkeeping (`goToRevisionIfEnabledCount++` + `BroadcastSlider.setSliderPosition(revision)`).
     - Update `padContents.currentRevision = revision` and `padContents.currentTime += timeDelta`.
     - Call `updateTimer()` and update authors UI as needed.
     - Return early (do **not** call the mutators).
   - Then remove (or relax) the `!isIdentity(...)` guards at the call sites.

2) Alternatively, add a small helper like `advanceRevision(revision, timeDelta, preventSliderMovement)` and call it in the `isIdentity` branches at each call site.

Either way, ensure live-following continues across identity revisions and `goToRevision()` ends with `padContents.currentRevision === path.rev` even if the composed changeset is identity.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

This .etherpad contents breaks timeslider playback

1 participant