Skip to content

feat(sync): choose the highest offered version this client can honour - #38

Merged
nxships merged 1 commit into
mainfrom
feat/contract-evolution
Aug 12, 2026
Merged

feat(sync): choose the highest offered version this client can honour#38
nxships merged 1 commit into
mainfrom
feat/contract-evolution

Conversation

@nxships

@nxships nxships commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Why

ContractResolution.ResolveAsync took the server's document unread. That is right until a version removes a field this client reads — at which point it has agreed to a document it cannot honour.

Now it walks the offered versions newest-first and takes the first one ClientSupport passes.

Falling behind is a normal outcome

Not a failure. The previous version keeps working, which is the entire reason old minors stay registered. ResolvedContract gained HighestOffered and Blockers, and IsBehind marks the case worth a log line.

The reason is returned rather than logged here — the client owns the log, and a silent downgrade is the one outcome nobody can diagnose afterwards.

Scopes are a parameter

Which collections the peer reads and which it writes decides what can break it. Null falls back to every scope the local document declares: safe, but pessimistic — it holds the client back over collections its key may not even touch. The real set comes from a handshake, which is why the caller now handshakes first and resolves after.

Cost

Descending order stops the search at the first version that fits. Describe returns the exact version asked for rather than negotiating, so a client already on the newest pays one request and a client that has to move pays two.

Floor

NexusKit.Sync raised to [0.6.0,) — where ClientSupport lives. Still an open range: pinning the protocol package is how a client ends up unable to follow a server that has moved on.

Verification

25 tests in localTools/tests/NexusKit.Modules.Sync.Tests, against an ISyncProtocol stub serving three versions — takes the newest it can, falls back and names the reason, keeps its own document when nothing fits, and counts the requests so a change that fetches everything up front would fail. Plus the writer/reader split: same removal, same two versions, opposite verdicts, decided only by the scope.

ResolveAsync took the server's document unread. That is right until a version removes a field
this client reads, at which point it has agreed to a document it cannot honour. Now it walks the
offered versions newest-first and takes the first one ClientSupport passes.

Falling behind is a normal outcome, not a failure: the previous version keeps working, which is
the entire reason old minors stay registered. ResolvedContract carries HighestOffered and
Blockers so the caller can say why, and IsBehind marks the case worth a log line. The reason is
returned rather than logged here because the client owns the log, and a silent downgrade is the
one outcome nobody can diagnose later.

Which collections the peer reads and which it writes decides what can break it, so grantedScopes
is a parameter. Null falls back to every scope the local document declares — safe but
pessimistic, holding the client back over collections its key may not even touch. The real set
comes from a handshake, which is why the caller now handshakes first and resolves after.

Descending order means the search stops at the first version that fits. Describe returns the
exact version asked for rather than negotiating, so a client already on the newest pays one
request and a client that has to move pays two.

Raises the NexusKit.Sync floor to 0.6.0, which is where ClientSupport lives.
@nxships
nxships merged commit 82f505b into main Aug 12, 2026
3 checks passed
@nxships
nxships deleted the feat/contract-evolution branch August 12, 2026 14:12
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.

1 participant