Library for building distributed, real-time collaborative web applications
MIT License
Bot releases are visible (Hide)
Published by github-actions[bot] 18 days ago
#22690
#22619
#22591
#22584
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.3.0...client_v2.3.1
Published by github-actions[bot] 18 days ago
#22654
#22620
#22590
#22573
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.2.1...client_v2.2.2
Published by github-actions[bot] 19 days ago
#22621
#22589
#22571
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.1.1...client_v2.1.2
Published by github-actions[bot] 23 days ago
#22622
#22606
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.8...client_v2.0.9
Published by github-actions[bot] 24 days ago
#22577
#22534
#22034
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.0-rc.5.0.7...client_v2.0.0-rc.5.0.8
Published by github-actions[bot] 25 days ago
#22588
#22568
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.7...client_v2.0.8
Published by github-actions[bot] 26 days ago
#22537
#22167
#22054
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.6...client_v2.0.7
Published by github-actions[bot] about 1 month ago
#22540
#22268
#22258
#22262
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.2.0...client_v2.2.1
Published by github-actions[bot] about 1 month ago
#22539
#22509
#22168
#21986
#21981
#21978
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.1.0...client_v2.1.1
Published by github-actions[bot] about 1 month ago
@beta
version of nodeChanged
which includes the list of properties has been added (#22229)@fluid-experimental/presence is now available for investigation. The new package is meant to support presence of collaborators connected to the same container. Use this library to quickly share simple, non-persisted data among all clients or send/receive fire and forget notifications.
API documentation for @fluid-experimental/presence is available at https://fluidframework.com/docs/apis/presence.
There are some limitations; see the README.md of installed package for most relevant notes.
We're just getting started. Please give it a go and share feedback.
Commit: 42b323c
Affected packages:
Factored event emitting utilities into their own file, events/emitter.ts
. Applications wishing to use SharedTree's eventing library for custom events can copy this file (and its referenced utility function) as a starting point for defining and emitting their own custom events. See createEmitter
's documentation for example usage.
Currently there are no published or officially supported versions of these utilities, but they are relatively simple, and can be copied and customized as needed.
Commit: 49849bb
Affected packages:
@beta
version of nodeChanged
which includes the list of properties has been added (#22229)const factory = new SchemaFactory("example");
class Point2d extends factory.object("Point2d", {
x: factory.number,
y: factory.number,
}) {}
const point = new Point2d({ x: 0, y: 0 });
TreeBeta.on(point, "nodeChanged", (data) => {
const changed: ReadonlySet<"x" | "y"> = data.changedProperties;
if (changed.has("x")) {
// ...
}
});
The payload of the nodeChanged
event emitted by SharedTree's TreeBeta
includes a changedProperties
property that indicates which properties of the node changed.
For object nodes, the list of properties uses the property identifiers defined in the schema, and not the persisted identifiers (or "stored keys") that can be provided through FieldProps
when defining a schema. See the documentation for FieldProps
for more details about the distinction between "property keys" and "stored keys".
For map nodes, every key that was added, removed, or updated by a change to the tree is included in the list of properties.
For array nodes, the set of properties will always be undefined: there is currently no API to get details about changes to an array.
Object nodes revieve strongly types sets of changed keys, allowing compile time detection of incorrect keys:
TreeBeta.on(point, "nodeChanged", (data) => {
// @ts-expect-error Strong typing for changed properties of object nodes detects incorrect keys:
if (data.changedProperties.has("z")) {
// ...
}
});
The existing stable "nodeChanged" event's callback now is given a parameter called unstable
of type unknown
which is used to indicate that additional data can be provided there. This could break existing code using "nodeChanged" in a particularly fragile way.
function f(optional?: number) {
// ...
}
Tree.on(point, "nodeChanged", f); // Bad
Code like this which is implicitly discarding an optional argument from the function used as the listener will be broken. It can be fixed by using an inline lambda expression:
function f(optional?: number) {
// ...
}
Tree.on(point, "nodeChanged", () => f()); // Safe
Commit: aae34dd
Affected packages:
SharedTree was not previously exported in a way that made it usable with @fluidframework/aqueduct or other lower-level legacy APIs. This fixes that issue by making it consistent with other DDSes: such usages can import { SharedTree } from "@fluidframework/tree/legacy";
.
Commit: bbdf869
Affected packages:
When determining if some given data is compatible with a particular ObjectNode schema, both inherited and own properties were considered. However, when constructing the node from this data, only own properties were used. This allowed input which provided required values in inherited fields to pass validation. When the node was constructed, it would lack these fields, and end up out of schema. This has been fixed: both validation and node construction now only consider own properties.
This may cause some cases which previously exhibited data corruption to now throw a usage error reporting the data is incompatible. Such cases may need to copy data from the objects with inherited properties into new objects with own properties before constructing nodes from them.
Commit: 27faa56
Affected packages:
fluid-framework/beta
now contains the @beta
APIs from @fluidframework/tree/beta
.
Commit: c51f55c
Affected packages:
@fluidframework/tree
and fluid-framework
now have a /alpha
import path where their @alpha
APIs are exported.
Commit: 12242cf
Affected packages:
There's a theoretical risk of indeterminate behavior due to a recent change to how batches of ops are processed. This fix reverses that change.
Pull Request #21785 updated the ContainerRuntime to hold onto the messages in an incoming batch until they've all arrived, and only then process the set of messages.
While the batch is being processed, the DeltaManager and ContainerRuntime's view of the latest sequence numbers will be out of sync. This may have unintended side effects, so out of an abundance of caution we're reversing this behavior until we can add the proper protections to ensure the system stays properly in sync.
Commit: 709f085
Affected packages:
The following packages will no longer be published:
PropertyDDS itself and its dependencies will continue to be published.
Commit: 8db660e
Affected packages:
Please continue to engage with us on GitHub Discussion and Issue pages as you adopt Fluid Framework!
Published by github-actions[bot] 2 months ago
Record
-typed objects can now be used to construct MapNodes (#22042)ITreeConfigurationOptions.preventAmbiguity
(#22048)@alpha
API FixRecursiveArraySchema
as a workaround around an issue with recursive ArrayNode schema (#22122)Tree.schema
now returns TreeNodeSchema
(#22185)isFluidHandle
type guard to check if an object is an IFluidHandle
(#22029)The isFluidHandle
type guard function is now exported and can be used to detect which objects are IFluidHandle
s. Since IFluidHandle
often needs special handling (for example when serializing since it's not JSON compatible), having a dedicated detection function for it is useful. Doing this detection was possible previously using the tree
package's schema system via Tree.is(value, new SchemaFactory("").handle)
, but can now be done with just isFluidHandle(value)
.
Commit: 7827d10
Affected packages:
Since the types for ArrayNodes and MapNodes indicate they can be constructed from iterables, it should work, even if those iterables are themselves arrays or maps. To avoid this being a breaking change, a priority system was introduced. ArrayNodes will only be implicitly constructable from JavaScript Map objects in contexts where no MapNodes are allowed. Similarly MapNodes will only be implicitly constructable from JavaScript Array objects in contexts where no ArrayNodes are allowed.
In practice, the main case in which this is likely to matter is when implicitly constructing a map node. If you provide an array of key value pairs, this now works instead of erroring, as long as no ArrayNode is valid at that location in the tree.
class MyMapNode extends schemaFactory.map("x", schemaFactory.number) {}
class Root extends schemaFactory.object("root", { data: MyMapNode }) {}
// This now works (before it compiled, but error at runtime):
const fromArray = new Root({ data: [["x", 5]] });
Prior versions used to have to do:
new Root({ data: new MyMapNode([["x", 5]]) });
or:
new Root({ data: new Map([["x", 5]]) });
Both of these options still work: strictly more cases are allowed with this change.
Commit: 25e74f9
Affected packages:
Record
-typed objects can now be used to construct MapNodes (#22042)You can now construct MapNodes from Record
typed objects, similar to how maps are expressed in JSON.
Before this change, an Iterable<string, Child>
was required, but now an object like {key1: Child1, key2: Child2}
is allowed.
Full example using this new API:
class Schema extends schemaFactory.map("ExampleMap", schemaFactory.number) {}
const fromRecord = new Schema({ x: 5 });
This new feature makes it possible for schemas to construct a tree entirely from JSON-compatible objects using their constructors, as long as they do not require unhydrated nodes to differentiate ambiguous unions, or IFluidHandles (which themselves are not JSON compatible).
Due to limitations of TypeScript and recursive types, recursive maps do not advertise support for this feature in their typing, but it works at runtime.
Commit: 25deff3
Affected packages:
ArrayNodes and MapNodes could always be explicitly constructed (using new
) from iterables. The types also allowed using of iterables to implicitly construct array nodes and map nodes, but this did not work at runtime. This has been fixed for all cases except implicitly constructing an ArrayNode form an Iterable
that is actually a Map
, and implicitly constructing a MapNode from an Iterable
that is actually an Array
. These cases may be fixed in the future, but require additional work to ensure unions of array nodes and map nodes work correctly.
Additionally MapNodes can now be constructed from Iterator<readonly [string, content]>
where previously the inner arrays had to be mutable.
Commit: 977f96c
Affected packages:
Before this fix, if multiple users concurrently performed moves (possibly by reverting prior moves), there was a chance that the document would become corrupted.
Commit: f3af9d1
Affected packages:
TreeViewConfiguration
is @sealed
, meaning creating custom implementations of it such as assigning object literals to a TreeViewConfiguration
or sub-classing it are not supported. This reserved the ability for the Fluid Framework to add members to this class over time, informing users that they must use it in such a way where such changes are non-breaking. However, there was no compiler-based enforcement of this expectation. It was only indicated via documentation and an implicit assumption that when an API takes in a typed defined as a class, that an instance of that class must be used rather than an arbitrary object of a similar shape.
With this change, the TypeScript compiler will now inform users when they invalidly provide an object literal as a TreeViewConfiguration
.
More specifically this causes code like this to produce a compile error:
// Don't do this!
const view = tree.viewWith({ schema: TestNode, enableSchemaValidation: false });
The above was never intended to work, and is not a supported use of the viewWith
since it requires a TreeViewConfiguration
which is sealed. Any code using the above pattern will break in Fluid Framework 2.2 and above. Such code will need to be updated to the pattern shown below. Any code broken by this change is technically unsupported and only worked due to a gap in the type checking. This is not considered a breaking change. The correct way to get a TreeViewConfiguration
is by using its constructor:
// This pattern correctly initializes default values and validates input.
const view = tree.viewWith(new TreeViewConfiguration({ schema: TestNode }));
Skipping the constructor causes the following problems:
TreeViewConfiguration
does validation in its constructor, so skipping it also skips the validation which leads to much less friendly error messages for invalid schema.enableSchemaValidation
. This means that code written in that style would break if more options were added. Since such changes are planned, it is not practical to support this pattern.Commit: e895557
Affected packages:
ITreeConfigurationOptions.preventAmbiguity
(#22048)The new ITreeConfigurationOptions.preventAmbiguity
flag can be set to true to enable checking of some additional rules when constructing the TreeViewConfiguration
.
This example shows an ambiguous schema:
const schemaFactory = new SchemaFactory("com.example");
class Feet extends schemaFactory.object("Feet", {
length: schemaFactory.number,
}) {}
class Meters extends schemaFactory.object("Meters", {
length: schemaFactory.number,
}) {}
const config = new TreeViewConfiguration({
// This combination of schema can lead to ambiguous cases, and will error since preventAmbiguity is true.
schema: [Feet, Meters],
preventAmbiguity: true,
});
const view = tree.viewWith(config);
// This is invalid since it is ambiguous which type of node is being constructed.
// The error thrown above when constructing the TreeViewConfiguration is because of this ambiguous case:
view.initialize({ length: 5 });
See the documentation on ITreeConfigurationOptions.preventAmbiguity
for a more complete example and more details.
Commit: 966906a
Affected packages:
@alpha
API FixRecursiveArraySchema
as a workaround around an issue with recursive ArrayNode schema (#22122)Importing a recursive ArrayNode schema via a d.ts file can produce an error like error TS2310: Type 'RecursiveArray' recursively references itself as a base type.
if using a tsconfig with "skipLibCheck": false
.
This error occurs due to the TypeScript compiler splitting the class definition into two separate declarations in the d.ts file (one for the base, and one for the actual class). For unknown reasons, splitting the class declaration in this way breaks the recursive type handling, leading to the mentioned error.
Since recursive type handling in TypeScript is order dependent, putting just the right kind of usages of the type before the declarations can cause it to not hit this error. For the case of ArrayNodes, this can be done via usage that looks like this:
/**
* Workaround to avoid
* `error TS2310: Type 'RecursiveArray' recursively references itself as a base type.` in the d.ts file.
*/
export declare const _RecursiveArrayWorkaround: FixRecursiveArraySchema<
typeof RecursiveArray
>;
export class RecursiveArray extends schema.arrayRecursive("RA", [
() => RecursiveArray,
]) {}
{
type _check = ValidateRecursiveSchema<typeof RecursiveArray>;
}
Commit: 9ceacf9
Affected packages:
[!WARNING]
This API is alpha quality and may change at any time.
Adds alpha-quality support for canonical JSON Schema representation of Shared Tree schema and adds a getJsonSchema
function for getting that representation for a given TreeNodeSchema
. This JSON Schema representation can be used to describe schema requirements to external systems, and can be used with validation tools like ajv to validate data before inserting it into a SharedTree
.
Given a SharedTree
schema like the following:
class MyObject extends schemaFactory.object("MyObject", {
foo: schemaFactory.number,
bar: schemaFactory.optional(schemaFactory.string),
});
JSON Schema like the following would be produced:
{
"$defs": {
"com.fluidframework.leaf.string": {
"type": "string"
},
"com.fluidframework.leaf.number": {
"type": "number"
},
"com.myapp.MyObject": {
"type": "object",
"properties": {
"foo": { "$ref": "com.fluidframework.leaf.number" },
"bar": { "$ref": "com.fluidframework.leaf.string" }
},
"required": ["foo"]
}
},
"anyOf": [{ "$ref": "#/$defs/com.myapp.MyObject" }]
}
Commit: 9097bf8
Affected packages:
Tree.schema
now returns TreeNodeSchema
(#22185)The typing of Tree.schema
has changed from:
schema<T extends TreeNode | TreeLeafValue>(node: T): TreeNodeSchema<string, NodeKind, unknown, T>;
to:
schema(node: TreeNode | TreeLeafValue): TreeNodeSchema;
The runtime behavior is unaffected: any code which worked and still compiles is fine and does not need changes.
Tree.schema
was changed to mitigate two different issues:
Foo | Bar
, or TreeNode
or even TreeNode | TreeLeafValue
), this was fine since schema are covariant over their node type. However when the input was more specific that the schema type, for example the type is simply 0
, this would result in unsound typing, since the create function could actually return values that did not conform with that schema (for example schema.create(1)
for the number schema typed with 0
would return 1
with type 0
).TNode
parameter is the third one, not the fourth. The fourth is TBuild
which sets the input accepted to its create function or constructor. Thus this code accidentally left TNode
unset (which is good due to the above issue), but invalidly set TBuild
. TBuild
is contravariant, so it has the opposite issue that setting TNode
would have: if your input is simply typed as something general like TreeNode
, then the returned schema would claim to be able to construct an instance given any TreeNode
. This is incorrect, and this typing has been removed.Fortunately it should be rare for code to be impacted by this issue. Any code which manually specified a generic type parameter to Tree.schema()
will break, as well as code which assigned its result to an overly specifically typed variable. Code which used typeof
on the returned schema could also break, though there are few use-cases for this so such code is not expected to exist. Currently it's very difficult to invoke the create function or constructor associated with a TreeNodeSchema
as doing so already requires narrowing to TreeNodeSchemaClass
or TreeNodeSchemaNonClass
. It is possible some such code exists which will need to have an explicit cast added because it happened to work with the more specific (but incorrect) constructor input type.
Commit: bfe8310
Affected packages:
TreeNode
's schema-aware APIs implement WithType
, which now has a NodeKind
parameter that can be used to narrow TreeNode
s based on NodeKind
.
Example:
function getKeys(node: TreeNode & WithType<string, NodeKind.Array>): number[];
function getKeys(
node: TreeNode & WithType<string, NodeKind.Map | NodeKind.Object>,
): string[];
function getKeys(node: TreeNode): string[] | number[];
function getKeys(node: TreeNode): string[] | number[] {
const schema = Tree.schema(node);
switch (schema.kind) {
case NodeKind.Array: {
const arrayNode = node as TreeArrayNode;
const keys: number[] = [];
for (let index = 0; index < arrayNode.length; index++) {
keys.push(index);
}
return keys;
}
case NodeKind.Map:
return [...(node as TreeMapNode).keys()];
case NodeKind.Object:
return Object.keys(node);
default:
throw new Error("Unsupported Kind");
}
}
Commit: 4d3bc87
Affected packages:
Consider a recursive SharedTree schema like the following, which follows all our recommended best practices:
export class RecursiveMap extends schema.mapRecursive("RM", [
() => RecursiveMap,
]) {}
{
type _check = ValidateRecursiveSchema<typeof RecursiveMap>;
}
This schema would work when used from within its compilation unit, but would generate d.ts that fails to compile when exporting it:
declare const RecursiveMap_base: import("@fluidframework/tree").TreeNodeSchemaClass<
"com.example.RM",
import("@fluidframework/tree").NodeKind.Map,
import("@fluidframework/tree").TreeMapNodeUnsafe<
readonly [() => typeof RecursiveMap]
> &
import("@fluidframework/tree").WithType<"com.example.RM">,
{
[Symbol.iterator](): Iterator<[string, RecursiveMap], any, undefined>;
},
false,
readonly [() => typeof RecursiveMap]
>;
export declare class RecursiveMap extends RecursiveMap_base {}
This results in the compile error in TypeScript 5.4.5:
error TS2310: Type 'RecursiveMap' recursively references itself as a base type.
With this change, that error is fixed by modifying the TreeMapNodeUnsafe
type it references to inline the definition of ReadonlyMap
instead of using the one from the TypeScript standard library.
Commit: 554fc5a
Affected packages:
The summarizeProtocolTree
property in ILoaderOptions was added to test single-commit summaries during the initial implementation phase. The flag is no longer required and should no longer be used, and is now marked deprecated. If a driver needs to enable or disable single-commit summaries, it can do so via IDocumentServicePolicies
.
Commit: 11ccda1
Affected packages:
These properties gcThrowOnTombstoneUsage
and gcTombstoneEnforcementAllowed
have been deprecated in IFluidParentContext
and ContainerRuntime
. These were included in certain garbage collection (GC) telemetry to identify whether the corresponding features have been enabled. These features are now enabled by default and this information is added to the "GarbageCollectorLoaded" telemetry.
Also, the following Garbage collection runtime options and configs have been removed. They were added during GC feature development to roll out and control functionalities. The corresponding features are on by default and can no longer be disabled or controlled:
GC runtime options removed:
gcDisableThrowOnTombstoneLoad
disableDataStoreSweep
GC configs removed:
"Fluid.GarbageCollection.DisableTombstone"
"Fluid.GarbageCollection.ThrowOnTombstoneUsage"
"Fluid.GarbageCollection.DisableDataStoreSweep"
Commit: b2bfed3
Affected packages:
The header InactiveResponseHeaderKey
is deprecated and will be removed in the future. It was part of an experimental feature where loading an inactive data store would result in returning a 404 with this header set to true. This feature is no longer supported.
Commit: 2e4e9b2
Affected packages:
The PropertyManager
class, along with the propertyManager
properties and addProperties
functions on segments and intervals, are not intended for external use. These elements will be removed in a future release for the following reasons:
Commit: cbba695
Affected packages:
The SegmentGroupCollection
class, along with the segmentGroups
property and ack
function on segments, are not intended for external use. These elements will be removed in a future release for the following reasons:
Commit: cbba695
Affected packages:
This schema converter had several known issues and has been removed. Read the schema converter section of the package readme for more details.
Commit: 54e4b5e
Affected packages:
Please continue to engage with us on GitHub Discussion and Issue pages as you adopt Fluid Framework!
Published by github-actions[bot] 3 months ago
This is a minor release.
Published by github-actions[bot] 3 months ago
#21904
#21924
#21842
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.5...client_v2.0.6
Published by github-actions[bot] 3 months ago
#21829
#21903
#21925
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.0-rc.5.0.6...client_v2.0.0-rc.5.0.7
Published by github-actions[bot] 3 months ago
This is a minor release.
Published by github-actions[bot] 3 months ago
#21950
#21722
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.0-rc.4.0.9...client_v2.0.0-rc.4.0.10
Published by github-actions[bot] 3 months ago
In the 2.0 release of Fluid, the concrete class implementations for DDSes were hidden from Fluid's API surface. This made instanceof
checks fail to work correctly. There were ways to work around this in application code, but they involved boilerplate which required more understanding of Fluid internals than should be necessary.
There is now a drop-in replacement to instanceof
: the static .is()
method to SharedObjectKind
, which is available on all DDSes. For example:
// Works in Fluid Framework 1.0 but not in the initial release of Fluid Framework 2.0:
if (myObject instanceof SharedString) {
// do something
}
// In Fluid Framework 2.1 and beyond, that code can now be written like so:
if (SharedString.is(myObject)) {
// do something
}
TypeScript allows delete
on object node optional fields if the exactOptionalPropertyTypes
tsconfig setting is not enabled. This does not work correctly at runtime and now produces an informative error.
When arrayNode
s are edited concurrently during iteration, an error will be thrown.
SharedTree now supports garbage collection so that removed content is not retained forever. This is an internal change and users of SharedTree won't need to adapt any existing code.
This change could cause errors with cross-version collaboration where an older client does not send data that a newer version may need. In this case, a "refresher data not found" error will be thrown.
Users should see improved performance when calling the Tree.shortId
API. Identifier field keys are now cached in the schema for faster access.
Several cases of invalid usage patterns for tree APIs have gained improved error reporting, as well as improved documentation on the APIs detailing what usage is supported. These improvements include:
Unsupported usages of schema classes: using more than one schema class derived from a single SchemaFactory generated base class. This used to hit internal asserts, but now has a descriptive user-facing UsageError. Most of this work was done in 9fb3dcf.
Improved detection of when prior exception may have left SharedTree in an invalid state. These cases now report a UsageError including a reference to the prior exception. This was mainly done in 9fb3dcf and b77d530.
TreeNodes now have custom debug visualizers to improve the debug experience in NodeJS and in browsers. Note that custom formatters must be enabled in the browser developer tools for that visualizer to be used.
Some tinylicious-client APIs were marked beta in previous releases. These APIs are now correctly marked public and also sealed to indicate they are not to be implemented externally to Fluid Framework and not changed.
Updated APIs:
Previously, the arguments of Marker.fromJSONObject
and TextSegment.fromJSONObject
were of type any
. However, at runtime only certain types were expected and using other types would cause errors.
Now, the argument for the Marker implementation is of type IJSONSegment
and the argument for the TextSegment implementation is of type string | IJSONSegment
. This reflects actual runtime support.
This change should have no impact on existing code unless the code is using incorrect types. Such code already does not function and should be corrected.
Note that this is a documentation only change. There is no runtime or type-level impact.
Some top-level APIs within @fluidframework/map
and fluid-framework
have been updated to reflect their sealed/readonly nature. That is, they are not to be implemented externally to Fluid Framework and not changed. This was already the case, but the documentation was not clear.
Updated APIs:
Please continue to engage with us on GitHub Discussion and Issue pages as you adopt Fluid Framework!
Published by github-actions[bot] 3 months ago
#21794
#21781
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.4...client_v2.0.5
Published by github-actions[bot] 3 months ago
#21793
#21741
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.0-rc.5.0.5...client_v2.0.0-rc.5.0.6
Published by github-actions[bot] 4 months ago
#21703
#21755
#21743
Full Changelog: https://github.com/microsoft/FluidFramework/compare/client_v2.0.3...client_v2.0.4