Skip to content

feat(tree): enablable allowed types #24631

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: main
Choose a base branch
from

Conversation

jenn-le
Copy link
Contributor

@jenn-le jenn-le commented May 15, 2025

Description

Adds enablable API to SchemaFactoryAlpha. This creates an allowed type that can be loaded into a tree from the document but not written to the tree.

@Copilot Copilot AI review requested due to automatic review settings May 15, 2025 05:41
@jenn-le jenn-le requested a review from a team as a code owner May 15, 2025 05:41
@github-actions github-actions bot added public api change Changes to a public API base: main PRs targeted against main branch labels May 15, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces an “enablable” annotation to SchemaFactoryAlpha so that certain types can be loaded (read) but not written to the tree, and wires that support through the low-level mapTreeFromNodeData API and related utilities.

  • Adds enablable(...) to SchemaFactoryAlpha and corresponding metadata type (SchemaUpgradeToken).
  • Extends mapTreeFromNodeData (and callers) with a new allowNonEnabledTypes option and updates default-injection and validation paths.
  • Updates tests to invoke the new options-object signature and adds scenarios for enablable types.

Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

File Description
src/simple-tree/api/schemaFactoryAlpha.ts Added enablable method and updated imports for annotated types
src/simple-tree/toMapTree.ts Introduced MapTreeFromNodeDataOptions, propagated allowNonEnabledTypes through conversion
api-report/tree.alpha.api.md Documented enablable API and SchemaUpgradeToken export
src/test/simple-tree/toMapTree.spec.ts (and others) Adapted existing tests to the new options object for mapTreeFromNodeData
Comments suppressed due to low confidence (2)

packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts:63

  • [nitpick] The parameter name t is ambiguous. Consider renaming it to schemaType or typeToEnable for clearer intent.
public enablable<const T extends TreeNodeSchema>(t: T | AnnotatedAllowedType<T>): AnnotatedAllowedType<T> {

packages/dds/tree/src/simple-tree/toMapTree.ts:91

  • You’ve introduced the allowNonEnabledTypes flag in mapTreeFromNodeData. Consider adding unit tests that verify both true and false behaviors to ensure that enablable types are correctly blocked or permitted.
allowNonEnabledTypes?: boolean;

/**
* A flag that determines if an enablable allowed type is permitted in the node data, defaults to false.
*/
allowNonEnabledTypes?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's important that that you don't have to enable all types at once.

If I have two different teams working on the same application, and both are working on features that use an enablable type, they should be able to enable them one at a time, and not have to wait for the feature to ship to start the second one or do them both at exactly the same time.

This I think you need to either provide a schema which reflects whats actually allowed in the document (the stored schema that's part of the optional schema nd policy above does that btw, but I'm going to be removing that from toMap tree most likley) OR provide a set of enablements (and which on must be able to be referred to somehow, which we need anyway to be able to do schema upgrades one at a time anyway).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This flag is just an implementation detail of the mapTreeFromNodeData funtion (which correct me if I'm wrong, I don't think is public) and is used to control when encountering an enablable causes a runtime error so that we can not error when loading the data.

Copy link
Contributor

Choose a reason for hiding this comment

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

But if we have a document where one type has been enabled, and another has not yet, how do we error if there is an instance of the no enabled one and not error if there is an instance of the enabled one?

Of do we just always error, in which case why is there a flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As of now, we either error on all enablables or none. Once schema upgrade is implemented, there will be a check for if an enabled has been upgraded and the flag needs to be false and the enablable not upgraded for there to be a runtime error.

In this implementation though, we don't error when we see an enablable in view.initialize so that we can tolerate enablables in documents. We error in every other case, which is when writing occurs.

Copy link
Contributor

Choose a reason for hiding this comment

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

don't error when we see an enablable in view.initialize

This seems like a case that should get a test in src/test/shared-tree/schematizingTreeView.spec.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good point, I had put it in sharedTree.spec.ts but def should have one at the schematizingTreeView layer as well.

@github-actions github-actions bot added the area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct label May 16, 2025
Copy link
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output


> [email protected] 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"


> [email protected] serve
> docusaurus serve --no-open

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

> [email protected] check-links
> linkcheck http://localhost:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  210260 links
    1663 destination URLs
    1895 URLs ignored
       0 warnings
       0 errors


This adds the `enablable` API to [`SchemaFactoryAlpha`](https://fluidframework.com/docs/api/fluid-framework/schemafactoryalpha-class).
Enablables can be used for schema evolution to add members to an [`AllowedTypes`](https://fluidframework.com/docs/api/fluid-framework/allowedtypes-typealias) while supporting cross version collaboration.

Enablables are allowed types that can be read by the tree but not written. Attempting to write an enablable type will cause a runtime error.
Copy link
Contributor

Choose a reason for hiding this comment

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

This need more clarity. If I have one client which has finished the migration (did a schema upgrade) and one client which still has the enableable wrapper, what happens?

Also what does "write" mean? In this cross version collab case, can I modify an existing value if the enableable type? You can right?

Can I close a subtree that contains it and insert that clone?

Assuming that the ability to insert the type in that location is controlled by the current state of the stored schema, I think we should adopt phrasing more like:

Suggested change
Enablables are allowed types that can be read by the tree but not written. Attempting to write an enablable type will cause a runtime error.
Enablables are allowed types that can be enabled by schema upgrades.
Before being enabled, any attempt to insert or move a node to a location which requires the enablement for its type to be valid will throw an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the updated phrasing a lot. I was struggling to make this sound correct :)

Can I close a subtree that contains it and insert that clone?

I haven't thought about this scenario and I'm not sure if I understand it. What does closing a subtree and cloning it look like in the code?

Copy link
Contributor

Choose a reason for hiding this comment

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

If someone does mynode.child = TreeBeta.clone(myNode.child); you might expect it to replace the tree with an identical one, not error if it contains new stuff.

Same for if someone does mynode.child = TreeAlpha.importVerbose(TreeAlpha.exportVerbose(myNode.child);

It seems odd if such cases error. It sounds like the intention is for them to error, which is ok for now, but we should make it clear that this approach can cause such errors.

@@ -163,9 +163,21 @@ export interface AllowedTypeMetadata {
*/
readonly custom?: unknown;

// TODO metadata for enablable types will be added here
/**
* If defined, indicates that an allowed type is enablable. Before upgrade, this type can be loaded into the tree from a document
Copy link
Contributor

Choose a reason for hiding this comment

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

Same note about "writable" from the changeset applies here.

Saying it may or may not be allowed based on enablement via the stored schema would make more sense.

Would also be good to clarify if enableable types are allowed in unhydrated trees (and error on hydration), or if they are errors to put in unhydrated trees.

destinationContext: FlexTreeContext,
allowNonEnabledTypes = false,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it makes sense for this to be a parameter here (or for any of the public function sin this file).

The job of this file is to be a uniform place to enforce consistent handling of insertions. There is no reason we would want different places calling this function to be have differently. It should make the choice internally based on some policy.

I'm not clear what that policy is currently supposed to be (are they enabled in unhydrated contexts? When actually hydrating do we unconditionally disable them?). This code has enough information to make those choices and implement those polices so it should be done here, in my opinion.

@@ -826,15 +826,15 @@ type Insertable<T extends ImplicitAllowedTypes> = readonly (
| IterableTreeArrayContent<InsertableTreeNodeFromImplicitAllowedTypes<T>>
)[];

abstract class CustomArrayNodeBase<const T extends ImplicitAllowedTypes>
abstract class CustomArrayNodeBase<const T extends ImplicitAnnotatedAllowedTypes>
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to see a unit test added to src/test/simple-tree/arrayNode.spec.ts testing the case of moving a node to a location where it requires enablement.

Such moves need to error if the type has not been enabled for its destination yet. This might work as is, but should have a test to ensure it works and stays working.

We should also test such a move after the enablement has happened (another client did a schema upgrade). I'm not actually sure if the intention is to allow such a move or not (clarity on what is allowed in this case is missing from the docs) but either way a test sould be good.

Copy link
Contributor

Choose a reason for hiding this comment

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

We should also test such a move after the enablement has happened (another client did a schema upgrade). I'm not actually sure if the intention is to allow such a move or not.

This out of my lane, but my two cents is that it would be preferrable to NOT allow such a move since that would complicate the guarantees associated with this feature. No matter what we pick, I would bet that the vast majority of app authors would looks at a client whose view schema has enablable(Foo) to think "this client couldn't have attached any Foo instances in this field", so we may as well pick the implementation choice that matches that. I may be missing some strong incentive to go the other way though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thats fine with me. As long as we have some policy, which is documented and tested, and it ensures we don't corrupt documents with out of schema items getting moved in, I'm happy. Yann's suggestion sounds like a fine choice as long as we test and enforce it.

Comment on lines 214 to 222
const annotations = annotatedAllowedTypes.get(schema);
if (annotations !== undefined) {
if (annotations.enabledUponSchemaUpgrade !== undefined) {
// TODO:#38722 check if the upgrade to enablable has happened, once the upgrade mechanism is implemented
throw new UsageError("Schema type is not enabled for this context");
}
} else {
throw new Error("Annotations should exist, at least as an empty object");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const annotations = annotatedAllowedTypes.get(schema);
if (annotations !== undefined) {
if (annotations.enabledUponSchemaUpgrade !== undefined) {
// TODO:#38722 check if the upgrade to enablable has happened, once the upgrade mechanism is implemented
throw new UsageError("Schema type is not enabled for this context");
}
} else {
throw new Error("Annotations should exist, at least as an empty object");
}
const annotations = annotatedAllowedTypes.get(schema) ?? fail("Annotations should exist, at least as an empty object");
if (annotations.enabledUponSchemaUpgrade !== undefined) {
// TODO:#38722 check if the upgrade to enablable has happened, once the upgrade mechanism is implemented
throw new UsageError("Schema type is not enabled for this context");
}

Once enough clients have this code update, it is safe to allow writing strings to the array.
To enable writing strings to the array, a code change must be made to remove the enablable annotation:
```typescript
schemaFactoryAlpha.arrayAlpha("TestArray", [schemaFactoryAlpha.number, schemaFactoryAlpha.string]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the inclusion of an example. I think we should also have an integration test showing this feature end to end in actual code (something like what's in src/test/feature-libraries/modular-schema/schemaEvolutionExamples.spec.ts except targeting the public API, and less horrible).

Making an executable example as a test, then including the same example in the changeset or doc comments is a great pattern to help ensure everything works as promised.

I'd like to see a test with some end to end proof involving a schema upgrade showing the whole process working. If thats too big for the changeset, it can have a simplified version, but link to the full exampe code via a github link.

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: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct base: main PRs targeted against main branch changeset-present public api change Changes to a public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants