Skip to content

Add first-writer-wins claims to data store runtime and DataObject#27286

Draft
kian-thompson wants to merge 5 commits into
microsoft:mainfrom
kian-thompson:users/copilot/claims-dataobject
Draft

Add first-writer-wins claims to data store runtime and DataObject#27286
kian-thompson wants to merge 5 commits into
microsoft:mainfrom
kian-thompson:users/copilot/claims-dataobject

Conversation

@kian-thompson
Copy link
Copy Markdown
Contributor

Description

Adds a new "claim" primitive to IFluidDataStoreRuntime and DataObject that lets a data store publish first-writer-wins key/value entries. A claim is conceptually a small piece of immutable per-data-store state where the first client to write a given key wins; concurrent writers from other clients observe "AlreadyClaimed" and can branch their logic accordingly.

Use a claim (rather than writing to DataObject.root) when you specifically need first-writer-wins semantics — for example, when multiple clients race to designate themselves as the owner of a particular role within the data store and only one should succeed.

New API surface (all @legacy @beta)

  • ClaimResult = "Success" | "AlreadyClaimed".
  • IFluidDataStoreRuntime.trySetClaim(key, value), getClaim(key), hasClaim(key), and claims (a read-only iterator).
  • IFluidDataStorePolicies.enableDataStoreClaims opt-in flag (defaults to off; set to true on the data store runtime to enable the API).
  • DataObject.trySetClaim, getClaim, hasClaim convenience helpers that forward to the runtime.

Implementation notes

  • Claim values may contain IFluidHandle instances; these are encoded the same way as handles in summary blobs and contribute outbound routes to garbage collection.
  • Claims are persisted via a .claims summary blob on the data store and rehydrated on subsequent loads.
  • New DataStoreMessageType.Claim op type. Older clients that don't understand it silently ignore it — authors enabling the flag should ensure all collaborators run a runtime that recognizes the op.
  • Resubmit / stashed-op paths handled; pending claims on a disposed runtime resolve to "AlreadyClaimed".

Testing

New claims.spec.ts suite covers: single-client success, two-client race, repeated calls from winner/loser, detached set + persistence, GC route collection from embedded handles, reconnect/resubmit, feature-flag-off, snapshot rehydration, staging-mode rejection, summary blob, and dispose behavior.

kian-thompson and others added 4 commits May 12, 2026 00:50
Introduces the public API surface for first-writer-wins claims:
- ClaimResult = "Success" | "AlreadyClaimed"
- IFluidDataStoreRuntime.trySetClaim/getClaim/hasClaim/claims (all optional)
- IFluidDataStorePolicies.enableDataStoreClaims opt-in flag

All members are @legacy @beta. Implementation follows in @fluidframework/datastore.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds first-writer-wins claim support to FluidDataStoreRuntime, gated by the
new IFluidDataStorePolicies.enableDataStoreClaims flag.

- Adds DataStoreMessageType.Claim op type and IClaimMessage envelope.
- New runtime state: sequencedClaims, wonClaims, pendingClaims, with async
  rehydration from a .claims summary blob.
- trySetClaim awaits load, then submits a Claim op (or writes directly when
  detached). Local op processing classifies the writer as winner/loser.
- Inbound Claim ops arriving before the snapshot load completes are buffered
  and drained in arrival order.
- Summary persistence via getAttachSummary() and summarize() writes the
  .claims blob; getOutboundRoutes walks claim values for handle routes so
  GC sees handles inside claims.
- Handle values inside claims are encoded/decoded the same way as in summary
  blobs (encodeHandleForSerialization / RemoteFluidObjectHandle).
- dispose() resolves any outstanding pending deferreds with AlreadyClaimed.
- Adds 13 unit tests covering single-client, two-client races, repeat-from-
  winner/loser, detached set + attach, GC routes, reconnect, feature flag
  off, staging mode, snapshot rehydration, summary content, and dispose.

The DataStoreMessageType enum gained a new value, which breaks the
current_as_old typetest; this is recorded in package.json typeValidation.broken.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Forwards to the underlying IFluidDataStoreRuntime claims API. Throws a
UsageError if the runtime does not implement claims (e.g., the
enableDataStoreClaims policy was not enabled when the data store was
created).

Updates the DataObject class doc-comment with guidance on when to use a
claim vs. writing to root.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (1171 lines, 16 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

@ChumpChief
Copy link
Copy Markdown
Contributor

This definitely feels like it's in the direction I would expect (right granularity and availability, right scenario support, etc.). Very much prefer this direction over #27291.

My main feedback would be about encapsulation and reuse of existing concepts. What you're building here is effectively a DDS - an improved and simplified version of ConsensusRegisterCollection. I'd be interested to see this actually built as a new standalone DDS, which we can then leverage in the DataStoreRuntime to provide the functionality. This would help keep the runtime from growing in direct responsibility, avoids expanding the op type vocabulary, probably simplifies your load flow and op buffering requirements, etc. It probably also sets you up well to leverage existing DDS testing framework, and avoids being a special case for features that intersect with the op stream (e.g. staging mode, rollback, etc.)

Such a DDS could start out as @internal since we don't have direct goals of providing the raw functionality at the moment, but I could also see it eventually being a public replacement that lets us deprecate ConsensusRegisterCollection.

Change trySetClaim from async Promise<ClaimResult> to a synchronous
method returning a discriminated-union IClaimAttempt:

  | { status: 'Success' | 'AlreadyClaimed' }
  | { status: 'Pending'; result: Promise<ClaimResult> }

Terminal cases (already-loaded + known outcome, or detached) return
synchronously with no promise. Cases where the outcome is not yet
known locally (claims still loading, or op submitted but not yet
sequenced — including attached+disconnected) return Pending with a
result promise that resolves when the outcome is determined.

Dispose now rejects pending claim deferreds with UsageError instead
of misleadingly resolving them to AlreadyClaimed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output


> fluid-framework-docs-site@0.0.0 ci:check-links /home/runner/work/FluidFramework/FluidFramework/docs
> start-server-and-test "npm run serve -- --no-open" 3000 check-links

1: starting server using command "npm run serve -- --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-docs-site@0.0.0 serve
> docusaurus serve --no-open

[SUCCESS] Serving "build" directory at: http://localhost:3000/

> fluid-framework-docs-site@0.0.0 check-links
> linkcheck http://localhost:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  288859 links
    1925 destination URLs
    2175 URLs ignored
       0 warnings
       0 errors


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.

2 participants