-
Notifications
You must be signed in to change notification settings - Fork 548
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
base: main
Are you sure you want to change the base?
Changes from all commits
060acdd
3d4d85e
fed6358
a8e69d5
5724351
984b829
4ecd8b7
1be4f30
90737e6
bcecd66
9a2036a
41f76db
e1acdd0
56f25e2
4d7fbfb
dc6e729
8858102
9c51607
cf3984e
7b0eeb7
e272d83
f42b75e
fb1704f
ce39396
cc76230
66021d5
c35b163
5a42cef
d792096
0d979aa
866ef86
f399aef
df586bf
574fd6c
56c8a8f
2ebe5fb
c2e0792
53b58f5
364511d
3065dd5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
--- | ||
"fluid-framework": minor | ||
"@fluidframework/tree": minor | ||
"__section": feature | ||
--- | ||
Adds enablable allowed types to SchemaFactoryAlpha | ||
|
||
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 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. | ||
|
||
To add a new member to an `AllowedTypes`, add the type wrapped by `enablable`. | ||
For example, migrating an array which previously supported only numbers to support both numbers and strings would start by deploying a version of the app using `enablable`: | ||
```typescript | ||
schemaFactoryAlpha.arrayAlpha("TestArray", [schemaFactoryAlpha.number, schemaFactoryAlpha.enablable(schemaFactoryAlpha.string)]); | ||
``` | ||
|
||
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]); | ||
``` | ||
|
||
In the future, SharedTree may add an API that allows enablables to be enabled via a runtime schema upgrade so that the type can be more easily deployed using a configuration flag change rather than a code change. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
extends TreeNodeWithArrayFeatures< | ||
Iterable<InsertableTreeNodeFromImplicitAllowedTypes<T>>, | ||
TreeNodeFromImplicitAllowedTypes<T> | ||
Iterable<InsertableTreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>>>, | ||
TreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>> | ||
> | ||
implements TreeArrayNode<T> | ||
implements TreeArrayNode<UnannotateImplicitAllowedTypes<T>> | ||
{ | ||
// Indexing must be provided by subclass. | ||
[k: number]: TreeNodeFromImplicitAllowedTypes<T>; | ||
[k: number]: TreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>>; | ||
|
||
public static readonly kind = NodeKind.Array; | ||
|
||
|
@@ -847,12 +847,16 @@ abstract class CustomArrayNodeBase<const T extends ImplicitAllowedTypes> | |
>; | ||
|
||
public constructor( | ||
input?: Iterable<InsertableTreeNodeFromImplicitAllowedTypes<T>> | InternalTreeNode, | ||
input?: | ||
| Iterable<InsertableTreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>>> | ||
| InternalTreeNode, | ||
) { | ||
super(input ?? []); | ||
} | ||
|
||
#mapTreesFromFieldData(value: Insertable<T>): ExclusiveMapTree[] { | ||
#mapTreesFromFieldData( | ||
value: Insertable<UnannotateImplicitAllowedTypes<T>>, | ||
): ExclusiveMapTree[] { | ||
const sequenceField = getSequenceField(this); | ||
const content = value as readonly ( | ||
| InsertableContent | ||
|
@@ -884,7 +888,9 @@ abstract class CustomArrayNodeBase<const T extends ImplicitAllowedTypes> | |
return fail(0xadb /* Proxy should intercept length */); | ||
} | ||
|
||
public [Symbol.iterator](): IterableIterator<TreeNodeFromImplicitAllowedTypes<T>> { | ||
public [Symbol.iterator](): IterableIterator< | ||
TreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>> | ||
> { | ||
return this.values(); | ||
} | ||
|
||
|
@@ -896,28 +902,33 @@ abstract class CustomArrayNodeBase<const T extends ImplicitAllowedTypes> | |
} | ||
|
||
public at( | ||
this: TreeArrayNode<T>, | ||
this: TreeArrayNode<UnannotateImplicitAllowedTypes<T>>, | ||
index: number, | ||
): TreeNodeFromImplicitAllowedTypes<T> | undefined { | ||
): TreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>> | undefined { | ||
const field = getSequenceField(this); | ||
const val = field.boxedAt(index); | ||
|
||
if (val === undefined) { | ||
return val; | ||
} | ||
|
||
return getOrCreateNodeFromInnerNode(val) as TreeNodeFromImplicitAllowedTypes<T>; | ||
return getOrCreateNodeFromInnerNode(val) as TreeNodeFromImplicitAllowedTypes< | ||
UnannotateImplicitAllowedTypes<T> | ||
>; | ||
} | ||
public insertAt(index: number, ...value: Insertable<T>): void { | ||
public insertAt( | ||
index: number, | ||
...value: Insertable<UnannotateImplicitAllowedTypes<T>> | ||
): void { | ||
const field = getSequenceField(this); | ||
validateIndex(index, field, "insertAt", true); | ||
const content = this.#mapTreesFromFieldData(value); | ||
field.editor.insert(index, content); | ||
} | ||
public insertAtStart(...value: Insertable<T>): void { | ||
public insertAtStart(...value: Insertable<UnannotateImplicitAllowedTypes<T>>): void { | ||
this.insertAt(0, ...value); | ||
} | ||
public insertAtEnd(...value: Insertable<T>): void { | ||
public insertAtEnd(...value: Insertable<UnannotateImplicitAllowedTypes<T>>): void { | ||
this.insertAt(this.length, ...value); | ||
} | ||
public removeAt(index: number): void { | ||
|
@@ -1057,12 +1068,14 @@ abstract class CustomArrayNodeBase<const T extends ImplicitAllowedTypes> | |
} | ||
} | ||
|
||
public values(): IterableIterator<TreeNodeFromImplicitAllowedTypes<T>> { | ||
public values(): IterableIterator< | ||
TreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>> | ||
> { | ||
return this.generateValues(getKernel(this).generationNumber); | ||
} | ||
private *generateValues( | ||
initialLastUpdatedStamp: number, | ||
): Generator<TreeNodeFromImplicitAllowedTypes<T>> { | ||
): Generator<TreeNodeFromImplicitAllowedTypes<UnannotateImplicitAllowedTypes<T>>> { | ||
const kernel = getKernel(this); | ||
if (initialLastUpdatedStamp !== kernel.generationNumber) { | ||
throw new UsageError(`Concurrent editing and iteration is not allowed.`); | ||
|
@@ -1113,7 +1126,7 @@ export function arraySchema< | |
|
||
// This class returns a proxy from its constructor to handle numeric indexing. | ||
// Alternatively it could extend a normal class which gets tons of numeric properties added. | ||
class Schema extends CustomArrayNodeBase<UnannotateImplicitAllowedTypes<T>> { | ||
class Schema extends CustomArrayNodeBase<T> { | ||
public static override prepareInstance<T2>( | ||
this: typeof TreeNodeValid<T2>, | ||
instance: TreeNodeValid<T2>, | ||
|
@@ -1200,8 +1213,8 @@ export function arraySchema< | |
return Schema.constructorCached?.constructor as unknown as Output; | ||
} | ||
|
||
protected get simpleSchema(): UnannotateImplicitAllowedTypes<T> { | ||
return unannotatedTypes; | ||
protected get simpleSchema(): T { | ||
return info; | ||
} | ||
protected get allowedTypes(): ReadonlySet<TreeNodeSchema> { | ||
return lazyChildTypes.value; | ||
|
There was a problem hiding this comment.
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.