Fluid Framework v2.41.0 (minor)
Contents
- ✨ New Features
- 🌳 SharedTree DDS Changes
- The comparePersistedSchema function (alpha) has had its canInitialize parameter removed (#24606)
- TreeAlpha.create now accepts unhydrated nodes (#24629)
- New TableSchema (alpha) APIs (#24579)
- SharedTrees's FluidClientVersion enum (alpha) has been redesigned (#24638)
- ForestTypeExpensiveDebug now validates content against schema (#24658)
- TreeNodes now implicitly generate identifiers on access instead of throwing (#24665)
✨ New Features
Presence APIs promoted to beta (#24710)
Presence APIs are now beta and can be imported via @fluidframework/presence/beta
.
Note: Notifications
are only supported via /alpha
imports. To access notifications-only workspace support, cast Presence
to PresenceWithNotifications
.
Change details
Commit: 6fce410
Affected packages:
- @fluidframework/presence
New experimental objectIdNumber API (#21115)
A new objectIdNumber
has been added, which is useful when you need an identifier which corresponds to an object identity. For example: when specifying a React "key" that corresponds to a TreeNode
.
Change details
Commit: df2f139
Affected packages:
- @fluid-experimental/tree-react-api
New TreeAlpha.key2 API (#24623)
The TreeAlpha.key2
method is meant to eventually replace the public Tree.key
method. This new method returns undefined
in the case where there is a root node.
Change details
Commit: 0ddd6b0
Affected packages:
- @fluidframework/tree
New TreeAlpha identifier APIs for converting, retrieving, and generating identifiers (#24218)
TreeAlpha.identifier
You can retrieve the long identifier with TreeAlpha.identifier(node)
, where node
is a TreeNode
. The long identifier is a stable, compressible UUID generated by the tree. In cases where the node does not yet have an identifier assigned, this will return undefined
. These cases include:
- The node does not contain an identifier field.
- The node is a non-hydrated node with a user provided identifier. Note that if it is a non-hydrated node without an identifier provided, it will throw an error.
TreeAlpha.identifier.shorten
You can shorten a long identifier with TreeAlpha.identifier.shorten(branch, identifier)
, where branch
is a TreeBranch
, and identifier
is a string
. If the method returns a valid short identifier, this identifier can be passed into TreeAlpha.identifier.lengthen
to get the original valid long identifier
back. In the cases where it's not possible to shorten the identifier
, it will return undefined
. These cases include:
- A compressible long identifier, but it is unrecognized by the tree that the node belongs to. This can occur if the identifier is not generated from the tree.
- An identifier which is not compressible by the tree. This can occur if the node's identifier was a user provided string.
TreeAlpha.identifier.lengthen
You can lengthen a short identifier with TreeAlpha.identifier.lengthen(branch, identifier)
, where branch
is a TreeBranch
, and identifier
is a number
. If the method returns a valid long identifier, this identifier can be passed into TreeAlpha.identifier.shorten
to get the original identifier
back. In the cases where it's not possible to lengthen the identifier
, this method will throw an error. These cases include:
- An unrecognized short identifier. This can occur if the identifier is not generated from the tree.
TreeAlpha.identifier.getShort
You can retrieve the short identifier from a node with TreeAlpha.identifier.getShort(node)
where node
is a TreeNode
. If it is not possible to retrieve the short identifier, it will return undefined
Example for a node with valid identifier
// This will retrieve the short identifier from the node.
const shortIdentifier = TreeAlpha.identifier.getShort(nodeWithValidIdentifier);
Examples for when you get undefined
In cases where the node provided does not contain an identifier that is recognized or compressible by the tree that the node belongs to, this method will return undefined. This will occur in the following cases:
- The node is an non-hydrated node with a user provided identifier. Note that if it is an non-hydrated node without an identifier provided, it will throw an error.
- The node does not contain an identifier field.
- The node contains a compressible long identifier, but it is unrecognized by the tree that the node belongs to. This can occur if the identifier is not generated from the tree.
- The node contains an identifier which is not compressible by its id compressor. This can occur if the node's identifier was a user provided string.
// This will return undefined
const shortIdentifier = TreeAlpha.identifier.getShort(node);
TreeAlpha.identifier.create
You can create a long identifier from a branch with TreeAlpha.identifier.create(branch)
where branch
is a TreeBranch
.
const createdIdentifier = TreeAlpha.identifier.create(branch);
Change details
Commit: e5b2882
Affected packages:
- fluid-framework
- @fluidframework/tree
"getPresence(container: IFluidContainer): Presence" now supported (#24399)
You can now use the getPresence
function to directly acquire Presence
. In previous releases, you were required to use ExperimentalPresenceManager
in container schema and calling getPresenceViaDataObject
, but that is no longer required. Both ExperimentalPresenceManager
and getPresenceViaDataObject
are now deprecated.
Change details
Commit: 5c6824a
Affected packages:
- @fluidframework/presence
🌳 SharedTree DDS Changes
The comparePersistedSchema function (alpha) has had its canInitialize parameter removed (#24606)
comparePersistedSchema has had its canInitialize
parameter removed. This parameter was only used to add to the output SchemaCompatibilityStatus. If a full SchemaCompatibilityStatus
is still desired, the canInitialize
value can be added to the result:
// old
const result = comparePersistedSchema(a, b, canInitialize);
// new
const result = { ...comparePersistedSchema(a, b), canInitialize };
Change details
Commit: d083a17
Affected packages:
- fluid-framework
- @fluidframework/tree
TreeAlpha.create now accepts unhydrated nodes (#24629)
TreeAlpha.create now accepts unhydrated nodes. TreeAlpha.create
's documentation has been updated to clarify that this is supported.
Additionally TreeAlpha.create
no longer throws a "Tree does not conform to schema" error when given a tree omitting an identifier. Instead, the identifier behaves like it would for other ways to build unhydrated nodes: remaining unreadable until hydrated.
Change details
Commit: e63af87
Affected packages:
- @fluidframework/tree
- fluid-framework
New TableSchema (alpha) APIs (#24579)
A TableSchema
utility has been added to Shared Tree for managing dynamic, tabular data. This new TableSchema
namespace contains APIs for creating column, row, and table node schema.
Note: these APIs require the use of SchemaFactoryAlpha.
Warning
These APIs are in preview and are subject to change. Until these APIs have stabilized, it is not recommended to use them in production code. There may be breaking changes to these APIs and their underlying data format. Using these APIs in production code may result in data loss or corruption.
Creating a table
You can craft a table schema with TableSchema.table
. This includes providing a schema for the cells that will appear in the table:
class MyTable extends TableSchema.table({
schemaFactory,
cell: schemaFactory.string,
}) {}
const table = new MyTable({
columns: [{ id: "column-0" }],
rows: [{ id: "row-0", cells: { "column-0": "Hello world!" } }],
});
Creating a table with custom column and row schema
To associate additional data with your rows or columns, generate custom row and column schema using TableSchema.column
and TableSchema.row
. These schema can then be provided to TableSchema.table
:
class MyColumn extends TableSchema.column({
schemaFactory,
cell: Cell,
props: schemaFactory.object("TableColumnProps", {
label: schemaFactory.string,
}),
}) {}
class MyRow extends TableSchema.row({
schemaFactory,
cell: Cell,
}) {}
class MyTable extends TableSchema.table({
schemaFactory,
cell: Cell,
column: MyColumn,
row: MyRow,
}) {}
const table = new MyTable({
columns: [
new MyColumn({ props: { label: "Entry" } }),
new MyColumn({ props: { label: "Date" } }),
new MyColumn({ props: { label: "Amount" } }),
],
rows: [],
});
Interacting with the table
Table trees created using TableSchema
offer various APIs to make working with tabular data easy. These include:
- Insertion and removal of columns, rows, and cells.
- Cell access by column/row.
// Create an empty table
const table = MyTable.empty();
const column0 = new MyColumn({
props: { label: "Column 0" },
});
// Append a column to the end of the table.
table.insertColumn({
column: column0,
});
const rows = [new MyRow({ cells: {} }), new MyRow({ cells: {} })];
// Insert rows at the beginning of the table.
table.insertRows({
index: 0,
rows,
});
// Set cell at row 0, column 0.
table.setCell({
key: {
column: column0,
row: rows[0],
},
cell: "Hello",
});
// Set cell at row 1, column 0.
table.setCell({
key: {
column: column0,
row: rows[1],
},
cell: "World",
});
// Remove the first row.
// Note: this will also remove the row's cell.
table.removeRow(rows[0]);
// Remove the column.
// Note: this will *not* remove the remaining cell under this column.
table.removeColumn(column0);
Listening for changes
Listening for changes to table trees behaves just like it would for any other nodes in a Shared Tree (see here for more details).
The most straightforward option is to listen for any changes to the table node and its descendants. For example:
class Cell extends schemaFactory.object("TableCell", {
value: schemaFactory.string,
}) {}
class Table extends TableSchema.table({
schemaFactory,
cell: Cell,
}) {}
const table = new Table({
columns: [{ id: "column-0" }],
rows: [{ id: "row-0", cells: {} }],
});
// Listen for any changes to the table and its children.
// The "treeChanged" event will fire when the `table` node or any of its descendants change.
Tree.on(table, "treeChanged", () => {
// Respond to the change.
});
If you need more granular eventing to meet your performance needs, that is possible as well. For example, if you wish to know when the table's list of rows changes, you could do the following:
class Cell extends schemaFactory.object("TableCell", {
value: schemaFactory.string,
}) {}
class Table extends TableSchema.table({
schemaFactory,
cell: Cell,
}) {}
const table = new Table({
columns: [{ id: "column-0" }],
rows: [{ id: "row-0", cells: {} }],
});
// Listen for any changes to the list of rows.
// The "nodeChanged" event will fire only when the `rows` node itself changes (i.e., its own properties change).
// In this case, the event will fire when a row is added or removed, or the order of the list is changed.
// But it won't fire when a row's properties change, or when the row's cells change, etc.
Tree.on(table.rows, "nodeChanged", () => {
// Respond to the change.
});
Limitations
Orphaned cells
Cells in the table may become "orphaned." That is, it is possible to enter a state where one or more rows contain cells with no corresponding column. To reduce the likelihood of this, you can manually remove corresponding cells when removing columns.
For example:
// Remove column1 and all of its cells.
// The "transaction" method will ensure that all changes are applied atomically.
Tree.runTransaction(table, () => {
// Remove column1
table.removeColumn(column1);
// Remove the cell at column1 for each row.
for (const row of table.rows) {
table.removeCell({
column: column1,
row,
});
}
});
Warning
Note that even with the above precaution, it is possible to enter such an orphaned cell state via the merging of edits. For example: one client might add a row while another concurrently removes a column, orphaning the cell where the column and row intersected.
Change details
Commit: e565f68
Affected packages:
- fluid-framework
- @fluidframework/tree
SharedTrees's FluidClientVersion enum (alpha) has been redesigned (#24638)
Users of FluidClientVersion's v2_1
, v2_2
, and v2_3
entries should specify v2_0
instead. This will result in no functional differences since no code currently opts into any additional functionality based on specifying those versions. The new approach avoids listing versions which there is currently no reason to select, and thus these options have been removed. If future work adds support to opt into features which only work starting with some of those versions, they will be re-added at that time.
Change details
Commit: 5f3b9d7
Affected packages:
- fluid-framework
- @fluidframework/tree
ForestTypeExpensiveDebug now validates content against schema (#24658)
When opting into using ForestTypeExpensiveDebug using configuredSharedTree, the tree is now checked against the schema on load and after every edit. This should help detect and diagnose document corruption bugs.
const DebugSharedTree = configuredSharedTree({
jsonValidator: typeboxValidator,
// Now detects corrupted documents which are out of schema.
forest: ForestTypeExpensiveDebug,
});
Change details
Commit: 9d600aa
Affected packages:
- @fluidframework/tree
- fluid-framework
TreeNodes now implicitly generate identifiers on access instead of throwing (#24665)
Accessing a defaulted identifier on an Unhydrated TreeNode
no longer throws a usage error. Instead, a new UUID is allocated for the identifier and returned. These UUIDs will be more compressible than random ones, since they all come from a single sequence (starting with a random UUID). They will not be fully compressed like the identifiers generated after hydration that leverage the document's IIdCompressor.
const factory = new SchemaFactory("test");
class HasIdentifier extends schema.object("A", { id: factory.identifier }) {}
// This used to throw an error:
const id = new HasIdentifier({}).id;
Change details
Commit: cd5976b
Affected packages:
- @fluidframework/tree
- fluid-framework
🛠️ Start Building Today!
Please continue to engage with us on GitHub Discussion and Issue pages as you adopt Fluid Framework!