Skip to content

Fluid Framework v2.41.0 (minor)

Compare
Choose a tag to compare
@github-actions github-actions released this 27 May 23:21
· 151 commits to main since this release
8fb28e1

Contents

✨ 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

⬆️ Table of contents

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

⬆️ Table of contents

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

⬆️ Table of contents

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

⬆️ Table of contents

"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

⬆️ Table of contents

🌳 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

⬆️ Table of contents

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

⬆️ Table of contents

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

⬆️ Table of contents

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

⬆️ Table of contents

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

⬆️ Table of contents

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

⬆️ Table of contents

🛠️ Start Building Today!

Please continue to engage with us on GitHub Discussion and Issue pages as you adopt Fluid Framework!