Skip to content

Introduce Unified ServiceClient API for Fluid Client#27693

Open
CraigMacomber wants to merge 33 commits into
microsoft:mainfrom
CraigMacomber:serviceClient
Open

Introduce Unified ServiceClient API for Fluid Client#27693
CraigMacomber wants to merge 33 commits into
microsoft:mainfrom
CraigMacomber:serviceClient

Conversation

@CraigMacomber

@CraigMacomber CraigMacomber commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

This add a simple API as a cleaner alternative to fluid-static and aquaduct.

The implementation details are well encapsulated, making this a viable alternative to the fluid-static public API and a viable successor to the existing much larger legacy API surface we do not want to stabilize.

This addresses several issues with our existing APIs:

  1. non-legacy (fluid-static declarative model) API:
    1. No stable way to use local service for testing (Internally or for customers). Forces tests to use alternative patterns (like test-utils, mocks etc. or a separate azure local service process).
    2. No way to avoid bundling in SharedDirectory.
    3. Defining and using DataStores is not well supported, especially for the root.
    4. Dependency layering is such that its disallowed for DDS packages to use the public API surfaces to do collab tests.
    5. No abstraction for Services / ServiceClient makes logic portable over services harder to write.
    6. Doesn't support lazy loading DDS and DataStore code (at least not clearly/cleanly).
    7. No migration path from legacy APIs to this
  2. legacy (encapsulated model, including aquaduct):
    1. Leak way too many details with too many non-sealed interfaces presenting tons of theoretical extension points we don't want to stabilize.
    2. Encourages use of DataObject and its subclassing based extension approach which is limiting (Like how it makes switching to root trees) hard, and makes changing the implementation without introducing breaking changes very difficult.

Because this new "unified" API offers value for our own internal testing (for example in tree), it is useful, even as alpha (or internal).

Exposing this new API as alpha should be low risk, as it makes no long-term commitments.

Since we have first party in-repo use cases, it should be practical to refine and iterate on this API with use in test and examples before we consider promoting it to beta.

Requirements for this new API:

  1. Testing:
    2. Can easily be used for tests including collab and reopening documents.
    3. Can be used by DDS (like shared-tree) tests replacing the need for many of our mocks and test utils
    4. Can be used by customers (apps and libraries requiring stable APIs when promoted past alpha). When adopted, customers should not have need of any of our legacy test-utils and mocks (so we can migrate our legacy apps off of our test utils and eventually make them internal only).
  2. Will be possible to stabilize to public (or at least beta) without depending on or stabilizing legacy APIs.
  3. Is easy to use for tests, including collab.
  4. Is easy to use for real production apps (should work the same as with tests).
  5. Provides a clear and practical incremental migration path from both existing APIs: there can be gaps we need to close for some use-cases but those gaps need to be practical to close.

Reviewer Guidance

The review process is outlined on this wiki page.

The example in the changeset is a good place to get started, followed by the examples/tests in the tree package.
Next I'd recomend reviewing the actual types for the API followed by its implementation.
See #27078 for many more examples of how this could be implemented in more examples and for other services.

CraigMacomber and others added 5 commits July 14, 2026 20:40
Introduces the foundational alpha public API for the new container
management layer:
- ServiceClient / ServiceContainerBase: top-level interface for
  creating and loading FluidContainers from any service
- FluidContainer / FluidContainerWithService / FluidContainerAttached:
  typed container handles with attach lifecycle
- DataStoreKind / DataStoreKey / DataStoreRegistry: type-safe wrappers
  around data store factories and their registries
- Registry / RegistryKey / basicKey / registryLookup: lightweight
  registry plumbing used by all ServiceClient implementations
- getContainerAudience: free function to obtain IAudience from an
  attached container without coupling to service-specific types

These types are exported at the alpha stability level from
@fluidframework/runtime-definitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ct-base

Adds the alpha factory layer that sits between shared objects (DDSs) and
the ServiceClient data store abstraction:
- dataStoreKind<T>(): wraps DataStoreOptions into a DataStoreKind with a
  fully wired IFluidDataStoreFactory under the hood
- DataStoreOptions<TRoot, TOutput>: configuration with type/registry/
  instantiateFirstTime/view callbacks
- DataStoreContext: object passed to instantiateFirstTime/view, implementing
  SharedObjectCreator for creating root and additional shared objects
- sharedObjectRegistryFromIterable(): convenience builder for ISharedObjectRegistry
- SharedObjectKey/SharedObjectKindAlpha: alpha variants of shared object
  type markers, allowing alpha DDS kinds to be used without leaking
  into the beta/public API surface
- StubSharedObject: minimal ISharedObject for testing that does not
  require a full FluidDataStoreRuntime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also exports the new ServiceClient / DataStore / SharedObject API surface
through the fluid-framework aggregator package.

tree's treeDataStoreKind is rolled up via fluid-framework's
`export * from "@fluidframework/tree/alpha"`, so the types referenced by its
signature must be re-exported from fluid-framework for the aggregator's API
Extractor rollup to resolve them. These re-exports depend only on
driver-definitions and shared-object-base, so they belong with (and are
required by) this commit.
Introduces serviceClientUtils.ts, shared internal infrastructure consumed
by all concrete ServiceClient implementations:
- makeServiceClientImpl(): generic factory that produces a ServiceClient
  from a service-specific ServiceContainerBase class and options
- makeCodeLoader(): builds the ICodeDetailsLoader that wires a
  ContainerRuntime to a service's data store registry
- convertRegistry() / normalizeRegistry(): adapt DataStoreRegistry to the
  IFluidDataStoreRegistry interface expected by ContainerRuntime
- rootDataStoreId: well-known alias for the root data store ("root")
- defaultMinVersionForCollab: default minimum collaboration version ("2.0.0")

Exports everything from @fluidframework/runtime-utils/internal so service
client packages can import without depending on each other.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds createEphemeralServiceClient() to @fluidframework/local-driver, an
implementation of ServiceClient backed entirely by an in-memory
LocalDeltaConnectionServer. Intended for unit and integration tests that
need a real collaborative container lifecycle without any network:
- createEphemeralServiceClient(): factory producing a ServiceClient whose
  containers run in a shared in-memory server scoped to the process
- closeEphemeralContainers(): cleanup helper that closes all containers
  and clears timer handles, preventing mocha from hanging after tests
- synchronizeLocalService(): test utility that flushes pending ops across
  all connected containers so tests can assert stable collaborative state

Also adds ephemeralServiceClient.spec.ts covering attach, close, shared
server, and multi-attach uniqueness, and treeDataStore.spec.ts which
tests the treeDataStoreKind() integration with the ephemeral service
(detach/attach lifecycle, collaboration, schema evolution, lazy loading,
and non-root data stores).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CraigMacomber CraigMacomber changed the title ServiceClient Introduce Unified API for Fluid Client Jul 14, 2026
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

Based on the diff (2966 lines, 39 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

@CraigMacomber CraigMacomber changed the title Introduce Unified API for Fluid Client Introduce Unified ServiceClient API for Fluid Client Jul 14, 2026
@CraigMacomber
CraigMacomber marked this pull request as ready for review July 15, 2026 04:15
@CraigMacomber
CraigMacomber requested review from a team as code owners July 15, 2026 04:15
Copilot AI review requested due to automatic review settings July 15, 2026 04:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot reviewed 36 out of 37 changed files in this pull request and generated no comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread packages/runtime/runtime-utils/src/serviceClientBase.ts Outdated
Comment on lines +40 to +61
/**
* Type guard for narrowing unions which contain DataStoreKind<T> to DataStoreKind<T>.
*/
public static guard<T>(
value: T,
): value is DataStoreKindImplementation<T extends DataStoreKind<infer T2> ? T2 : never> & T {
return value instanceof DataStoreKindImplementation;
}

/**
* Type guard for narrowing unions which contain DataStoreKind<T> to DataStoreKind<T>.
*/
public static narrowGeneric<T>(
value: T,
): asserts value is DataStoreKindImplementation<
T extends DataStoreKind<infer T2> ? T2 : never
> &
T {
if (!DataStoreKindImplementation.guard(value)) {
throw new Error("Invalid DataStoreKindImplementation");
}
}

@Josmithr Josmithr Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: any reason these can't be free functions instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They could be. I think it makes a for a much nicer API for them to be statics. This puts them right next to (API wise) the inherited static like narrow upCast and [hasInstance], and avoids cluttering the top level namespace with a bunch of utilities for working with this specific class. As they compile town to almost nothing, I don't think tree shaking should be a concern (which would be the main reason to make statics on an internal class into free functions)

Comment thread packages/runtime/runtime-utils/src/serviceClientBase.ts Outdated
Comment thread packages/runtime/runtime-utils/src/serviceClientUtils.ts Outdated
Comment thread packages/runtime/runtime-utils/src/serviceClientUtils.ts Outdated
export const minimize: TransactionPostProcessor;

// @alpha @input
export type MinimumVersionForCollaboration = `2.${bigint}.0`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@CraigMacomber it looks like we're migrating away from the "minimum version for collaboration" naming elsewhere. FYI.
#27656

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm going to leave this as open an unaddressed for now, waiting until either this is close to ready or that PR goes in to do a rename.

@github-actions github-actions Bot added area: examples Changes that focus on our examples area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct area: tools area: runtime Runtime related issues area: driver Driver related issues area: dds Issues related to distributed data structures area: repo Repo related work dependencies Pull requests that update a dependency file area: website public api change Changes to a public API area: dds: tree changeset-present base: main PRs targeted against main branch labels Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output

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-website@0.0.0 serve
> docusaurus serve --no-open

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

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

Crawling...

Stats:
  306030 links
    1955 destination URLs
    2209 URLs ignored
       0 warnings
       0 errors


@github-actions

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: 393ccf349a49001281c596921d8683847f285815
Head commit: 91f63dadde414f4c5cb054116263cc41fe3166f2

Notable changes

  • 🔴 azureClient.js: parsed 619911 → 620520 (+609), gzip 164958 → 165168 (+210)
  • 🔴 odspClient.js: parsed 592637 → 593257 (+620), gzip 159070 → 159285 (+215)
  • 🔴 aqueduct.js: parsed 526400 → 526999 (+599), gzip 140839 → 141039 (+200)
  • 🔴 fluidFramework.js: parsed 394822 → 395844 (+1022), gzip 112116 → 112442 (+326)
  • 🔴 sharedTree.js: parsed 384209 → 385241 (+1032), gzip 109499 → 109851 (+352)
Per-bundle deltas

@fluid-example/bundle-size-tests

  • 🔴 azureClient.js: parsed 619911 → 620520 (+609), gzip 164958 → 165168 (+210)
  • 🔴 odspClient.js: parsed 592637 → 593257 (+620), gzip 159070 → 159285 (+215)
  • 🔴 aqueduct.js: parsed 526400 → 526999 (+599), gzip 140839 → 141039 (+200)
  • 🔴 fluidFramework.js: parsed 394822 → 395844 (+1022), gzip 112116 → 112442 (+326)
  • 🔴 sharedTree.js: parsed 384209 → 385241 (+1032), gzip 109499 → 109851 (+352)
  • containerRuntime.js: parsed 304750 → 305068 (+318), gzip 83332 → 83411 (+79)
  • sharedString.js: parsed 175984 → 176229 (+245), gzip 49445 → 49548 (+103)
  • experimentalSharedTree.js: parsed 160798 → 160798 (0), gzip 45804 → 45804 (0)
  • matrix.js: parsed 159845 → 160090 (+245), gzip 45411 → 45510 (+99)
  • loader.js: parsed 145256 → 145270 (+14), gzip 39063 → 39076 (+13)
  • odspDriver.js: parsed 104329 → 104350 (+21), gzip 32625 → 32635 (+10)
  • directory.js: parsed 66616 → 66861 (+245), gzip 18532 → 18638 (+106)
  • 748.js: parsed 58793 → 58793 (0), gzip 17827 → 17827 (0)
  • map.js: parsed 46709 → 47056 (+347), gzip 14310 → 14444 (+134)
  • odspPrefetchSnapshot.js: parsed 45642 → 45656 (+14), gzip 15277 → 15284 (+7)
  • 985.js: parsed 44491 → 44491 (0), gzip 13726 → 13726 (0)
  • summarizerDelayLoadedModule.js: parsed 30749 → 30749 (0), gzip 7753 → 7753 (0)
  • socketModule.js: parsed 26476 → 26483 (+7), gzip 7887 → 7894 (+7)
  • createNewModule.js: parsed 12480 → 12480 (0), gzip 4786 → 4786 (0)
  • summaryModule.js: parsed 3797 → 3797 (0), gzip 1860 → 1860 (0)
  • connectionState.js: parsed 724 → 724 (0), gzip 429 → 429 (0)
  • sharedTreeAttributes.js: parsed 666 → 673 (+7), gzip 432 → 441 (+9)
  • debugAssert.js: parsed 429 → 429 (0), gzip 299 → 299 (0)
  • FluidFramework-HashFallback.js: parsed 422 → 422 (0), gzip 316 → 316 (0)

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

Labels

area: dds: tree area: dds Issues related to distributed data structures area: driver Driver related issues area: examples Changes that focus on our examples area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct area: repo Repo related work area: runtime Runtime related issues area: tools area: website base: main PRs targeted against main branch changeset-present dependencies Pull requests that update a dependency file public api change Changes to a public API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants