Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/claims-dds-shareddirectory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@fluidframework/map": minor
"__section": feature
---
Add first-writer-wins claim API on SharedDirectory (`trySetClaim`/`isClaimed`)

`ISharedDirectory` now exposes two new methods on the root directory:

- `trySetClaim(key, value): Promise<ClaimResult>` — Attempts to claim a root-level key with a value using first-writer-wins semantics across all clients. Resolves to `"Success"` if this client (or this client's already-pending attempt) won, or `"AlreadyClaimed"` if another client's claim was sequenced first.

Check failure on line 9 in .changeset/claims-dds-shareddirectory.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Dashes] Remove the spaces around ' — '. Raw Output: {"message": "[Microsoft.Dashes] Remove the spaces around ' — '.", "location": {"path": ".changeset/claims-dds-shareddirectory.md", "range": {"start": {"line": 9, "column": 50}}}, "severity": "ERROR"}
- `isClaimed(key): boolean` — Returns `true` if the root-level key is currently claimed.

Check failure on line 10 in .changeset/claims-dds-shareddirectory.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Dashes] Remove the spaces around ' — '. Raw Output: {"message": "[Microsoft.Dashes] Remove the spaces around ' — '.", "location": {"path": ".changeset/claims-dds-shareddirectory.md", "range": {"start": {"line": 10, "column": 28}}}, "severity": "ERROR"}

Once a key is claimed, the value is immutable: `set` and `delete` for that key throw `UsageError`, and `get` returns the claimed value. `clear()` does not remove claims. Claims survive summary/load.

This API is gated behind a runtime opt-in. To enable it, set `enableDdsClaims: true` on the data store runtime options. Calling `trySetClaim` or `isClaimed` without the flag enabled throws `UsageError`.

Claims are only available on the root `ISharedDirectory`; subdirectories (`IDirectory`) intentionally do not expose these methods.

Check warning on line 16 in .changeset/claims-dds-shareddirectory.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Semicolon] Try to simplify this sentence. Raw Output: {"message": "[Microsoft.Semicolon] Try to simplify this sentence.", "location": {"path": ".changeset/claims-dds-shareddirectory.md", "range": {"start": {"line": 16, "column": 57}}}, "severity": "INFO"}
13 changes: 13 additions & 0 deletions packages/dds/map/api-report/map.legacy.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

```ts

// @beta @legacy
export type ClaimResult = "Success" | "AlreadyClaimed";

// @beta @sealed @legacy
export class DirectoryFactory implements IChannelFactory<ISharedDirectory> {
static readonly Attributes: IChannelAttributes;
Expand All @@ -14,6 +17,13 @@ export class DirectoryFactory implements IChannelFactory<ISharedDirectory> {
get type(): string;
}

// @beta @legacy
export interface IClaimable<V = unknown> {
// (undocumented)
isClaimed(key: string): boolean;
trySetClaim(key: string, value: V): Promise<ClaimResult>;
}

// @beta @deprecated @legacy
export interface ICreateInfo {
ccIds: string[];
Expand Down Expand Up @@ -53,6 +63,7 @@ export interface IDirectoryEvents extends IEvent {
// @beta @deprecated @legacy
export interface IDirectoryNewStorageFormat {
blobs: string[];
claims?: [string, ISerializableValue][];
content: IDirectoryDataObject;
}

Expand All @@ -73,6 +84,8 @@ export interface ISharedDirectory extends ISharedObject<ISharedDirectoryEvents &
[Symbol.iterator](): IterableIterator<[string, any]>;
// (undocumented)
readonly [Symbol.toStringTag]: string;
isClaimed(key: string): boolean;
trySetClaim(key: string, value: unknown): Promise<ClaimResult>;
}

// @beta @sealed @legacy
Expand Down
6 changes: 5 additions & 1 deletion packages/dds/map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@
"typescript": "~5.4.5"
},
"typeValidation": {
"broken": {},
"broken": {
"TypeAlias_SharedDirectory": {
"forwardCompat": false
}
},
"entrypoint": "legacy"
}
}
47 changes: 47 additions & 0 deletions packages/dds/map/src/claims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

/**
* Result of a {@link IClaimable.trySetClaim} call.
*
* @remarks
* - `"Success"` indicates that this client's claim is the sequenced winner for the key.
* - `"AlreadyClaimed"` indicates that some other claim for the key was sequenced first;
* the key is now immutable for the lifetime of the document.
*
* @legacy @beta
*/
export type ClaimResult = "Success" | "AlreadyClaimed";

/**
* First-writer-wins ("claim") API for keyed singleton entries on a DDS.
*
* @remarks
* Once a key has been successfully claimed by any client, subsequent attempts to claim the
* same key from any client will resolve to {@link ClaimResult | `"AlreadyClaimed"`}. Writes
* to the same key via the DDS's normal mutation APIs (e.g. `set`/`delete`/`clear`) are
* dropped or rejected, so the claimed value is immutable for the lifetime of the document.
*
* This API is opt-in. Implementations gate access behind a runtime option (e.g.
* `runtime.options.enableDdsClaims`); when disabled, {@link IClaimable.trySetClaim} throws
* a `UsageError`.
*
* @legacy @beta
*/
export interface IClaimable<V = unknown> {
/**
* Attempt to publish `value` for `key` as a singleton.
*
* @returns A promise that resolves once the claim has been ack'd by the service:
* `"Success"` if this client won the race; `"AlreadyClaimed"` if some other claim was
* sequenced first.
*/
trySetClaim(key: string, value: V): Promise<ClaimResult>;

/**
* @returns `true` if `key` has been sequenced as claimed (by any client).
*/
isClaimed(key: string): boolean;
}
Loading
Loading