Skip to content

🐛 Reject presence whose version is ahead of the doc - #749

Open
alecgibson wants to merge 1 commit into
check-doc-presence-channelfrom
reject-future-presence-version
Open

alecgibson wants to merge 1 commit into
check-doc-presence-channelfrom
reject-future-presence-version

Conversation

@alecgibson

@alecgibson alecgibson commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #735

At the moment, the server relays a doc presence whose v is ahead of the doc unchanged, which lets one client make every subscriber fetch:

  1. Submit presence with v: 1e9 to a doc with 20 subscribers
  2. Each subscriber calls doc.fetch(), since v > doc.version
  3. The presence stays pending, so every later op fetches again

This change has the Agent pass its tracked doc version through to the presence transform, which reads ops from the lower of that and v. The tracker can lag the database but never lead it, so a v past the last op read is ahead of the doc. Ops in that lag may have been deleted, though, so we check the snapshot version before rejecting. Doc presence must now carry an integer v, and so must ops that send one. A rejected presence restores the Agent's previously cached request, so the leave it replays on disconnect isn't rejected too.

Presence sent without c/d skips all of this, since the server can't tell it from generic presence, but clients ignore it since #752. We deliberately don't bound presence that is far behind: a fetch with v: 0 already makes the server read the whole op history.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

@coveralls

coveralls commented Oct 6, 2026 •

Copy link
Copy Markdown

Coverage Status

Coverage is 97.965% — reject-future-presence-version into check-doc-presence-channel. No base build found for check-doc-presence-channel.

Comment thread lib/backend.js
@alecgibson
alecgibson force-pushed the reject-future-presence-version branch from 7f7fc81 to ae15459 Compare October 9, 2026 11:09
@alecgibson
alecgibson requested a balanced review from Copilot October 9, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Versionless database operations can bypass rejection, while rejected presence can leak subscriptions and cached state.

2 open findings
What changed in this PR

Rejects invalid or future document-presence versions to prevent repeated client fetch amplification.

Changes:

  • Requires integer operation and presence versions.
  • Validates presence against tracked or fetched document versions.
  • Adds presence-version and deleted-operation tests.
File Description
lib/​agent.js Validates versions and passes tracked versions for presence transformation.
lib/​backend.js Detects and rejects future presence versions.
test/​agent.js Tests fractional operation-version rejection.
test/​client/​presence/​doc-presence.js Tests presence validation and transformation scenarios.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/backend.js Outdated
Comment thread lib/agent.js Outdated
@alecgibson
alecgibson force-pushed the reject-future-presence-version branch from ae15459 to df41add Compare October 9, 2026 14:43
@alecgibson
alecgibson requested a balanced review from Copilot October 9, 2026 14:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Document-channel validation remains bypassable, and concurrent rejected requests can corrupt cached presence state.

3 open findings
2 resolved since last review

🧠 Review effort: Balanced

Comment thread lib/backend.js
Comment thread lib/agent.js
Comment thread lib/agent.js
@alecgibson
alecgibson force-pushed the reject-future-presence-version branch 2 times, most recently from ce43edc to 6ec1a77 Compare October 13, 2026 16:14
@alecgibson
alecgibson requested a balanced review from Copilot October 13, 2026 16:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Empty document coordinates bypass validation, and lagging trackers can reject valid presence after operation truncation.

1 open finding
3 resolved since last review

🧠 Review effort: Balanced

Comment thread lib/backend.js
@alecgibson
alecgibson force-pushed the reject-future-presence-version branch from 6ec1a77 to 95e7327 Compare October 13, 2026 16:59
@alecgibson
alecgibson requested a balanced review from Copilot October 13, 2026 17:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Coordinate filtering can be bypassed before remote-update dispatch, allowing mismatched presence to acknowledge local requests.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment thread lib/client/presence/doc-presence.js Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The implementation addresses the amplification path and includes extensive regression coverage.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

@alecgibson
alecgibson marked this pull request as ready for review October 14, 2026 16:00
Fixes #735

At the moment, the server relays a doc presence whose `v` is ahead of
the doc unchanged, which lets one client make every subscriber fetch:

 1. Submit presence with `v: 1e9` to a doc with 20 subscribers
 2. Each subscriber calls `doc.fetch()`, since `v > doc.version`
 3. The presence stays pending, so every later op fetches again

This change has the `Agent` pass its tracked doc version through to the
presence transform, which reads ops from the lower of that and `v`. The
tracker can lag the database but never lead it, so a `v` past the last
op read is ahead of the doc. Ops in that lag may have been deleted,
though, so we check the snapshot version before rejecting. Doc presence
must now carry an integer `v`, and so must ops that send one. A rejected
presence restores the `Agent`'s previously cached request, so the leave
it replays on disconnect isn't rejected too.

Presence sent without `c`/`d` skips all of this, since the server can't
tell it from generic presence, but clients ignore it since #752. We
deliberately don't bound presence that is far *behind*: a `fetch` with
`v: 0` already makes the server read the whole op history.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@alecgibson
alecgibson force-pushed the reject-future-presence-version branch from 95e7327 to 0a26a74 Compare October 14, 2026 16:30
@alecgibson
alecgibson changed the base branch from main to check-doc-presence-channel October 14, 2026 16:30
@alecgibson

Copy link
Copy Markdown
Collaborator Author

Migration note

Not breaking. This can ship as a 6.x 🐛 fix.

The server now rejects requests that the bundled client never sends:

  • Doc presence without a v, or with a non-integer one, is rejected as ERR_MESSAGE_BADLY_FORMED, and so is an op that sends a non-integer v. The client always sends v: doc.version.
  • Doc presence whose v is ahead of the doc is rejected with ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT instead of being relayed. The client's v comes from the server, so it's never ahead, unless your database adapter reads from a replica that can lag the primary. In that case, presence from a client that has seen ops the replica hasn't is now rejected rather than relayed.

The Agent also no longer calls Backend.prototype.transformPresenceToLatestVersion(). It's undocumented, but if you override it, the override no longer runs on presence.

This branch has not been deployed

No deployments
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.

Presence version is not clamped, amplifying work on server and other clients

3 participants