1.3.7 • Published 4 months ago

@fluid-experimental/tree v1.3.7

Weekly downloads
236
License
MIT
Repository
github
Last release
4 months ago

@fluid-experimental/tree

A Fluid SharedObject Tree with:

  • Transactional editing
  • Strong node identities
  • High quality automatic merge resolution
  • Full History Inspection, Manipulation and Metadata

Revisions of the tree (see EditLog and Snapshot) are created from sequences of Edits.

Semantically, the current state of the tree is defined as:

The initial tree, modified by all edits in order.

The order of the edits is:

  1. All acknowledged edits, in the order agreed upon by Fluid's consensus.
  2. All local edits (not acknowledged by Fluid yet), in the order they were created.

Maintenance

This package is currently being maintained in an internal Microsoft repository as well as in the Fluid Framework repository. As a result, there are some inconsistencies in development tooling configuration between this package and a typical Fluid package. The main difference is that code is formatted using prettier. Correct formatting can still be produced using the lint:fix script and is enforced with the lint script. For the most part, this means contributers can ignore these style differences. However, this dual maintenance should be kept in mind when considering changes to tooling configuration.

Getting Started

Tree Abstraction

The tree abstraction used for SharedTree is summarized by the Node class. Nodes have traits, a definition, an identity, and optionally a payload to contain extra arbitrary data.

Definition

The definition of a node provides the node's meaning. It is typically used to associate the node with metadata such as a schema for what this tree represents.

Identifier

A node's identifier is a globally unique way to refer to it. This enables collaborative editing specifications by ensuring that each element of a document can be identified.

Traits

Traits are what give a node the structure of a tree. Intuitively, a trait is a named sequence of content nodes. Organizing a node's children underneath traits (rather than in a freeform list, as many trees do) allows more natural construction of documents.

Example

Consider a document which consists of the point (4, 9) in 2-dimensional space. One way this document could be encoded into the tree format expected by a SharedTree might look like this:

// These definitions conceptually refer to *all* points/numbers in existence.
const pointDefinition = '3781c3b1-41e6-43c5-9ffd-13916071d4dc';
const numberDefinition = '3e5a1652-983f-4533-bb59-130ac8f3714e';

// These identifiers refer to *the particular* point/number nodes in the tree below.
const pointIdentifier = '668b4277-ed5b-41f6-90ce-d2f666a59e41';
const xIdentifier = 'f067342f-5307-460e-a67a-41d2448231f3';
const yIdentifier = '6a23b443-8735-4c1d-8f88-3aca5ba07939';

const pointDocument: Node = {
	definition: pointDefinition
	identifier: pointIdentifier,
	traits: {
		x: {
			definition: numberDefinition,
			identifier: xIdentifier,
			payload: 4
		},
		y: {
			definition: numberDefinition,
			identifier: yIdentifier,
			payload: 9
		}
	}
};

Note that this example isn't meant to be taken verbatim as valid code -- it cheats a bit with payload representation for the sake of simplicity. It is, however, truthful to tree structure.

Creating a SharedTree

SharedTree follows typical Fluid DDS conventions and can be constructed with a Fluid runtime instance:

const tree = SharedTree.create(runtime);

Upon creation, the tree will contain a single node: initialTree.

Editing

For simple edits (ones in which transactionality isn't important), SharedTree provides convenient, imperative APIs along the following lines:

tree.editor.insert(fooNode, StablePlace.atStartOf({ parent: initialTree.identifier, label: 'foo' }));
tree.editor.insert(barNode, StablePlace.atStartOf({ parent: initialTree.identifier, label: 'bar' }));

This would insert fooNode at the start of the "foo" trait underneath the initial tree's root node, and barNode underneath the "bar" trait. Each operation would be performed in its own Edit, which is SharedTree's transactional atom.

If it is undesirable that one of the above operations could fail to apply while the other could succeed, you should instead leverage Checkout. A Checkout--whose name is inspired from source control--can be thought of as a local view of the SharedTree which provides snapshot isolation. Editing the SharedTree using a Checkout can be done by opening an edit, applying a number of changes, and closing the edit.

const checkout = new BasicCheckout(tree);
checkout.openEdit();
checkout.applyChanges(Insert.create([fooNode], StablePlace.atStartOf({ parent: initialTree, label: 'foo' })));
checkout.applyChanges(Insert.create([barNode], StablePlace.atStartOf({ parent: initialTree, label: 'bar' })));
checkout.closeEdit();

"Move" and "delete" operations have the added complexity of needing to specify locations (StableRanges) within the SharedTree which should be moved (or deleted, respectively). A StableRange consists of a start StablePlace and an end StablePlace. StablePlaces are not nodes, but instead places where nodes could be inserted. Each place consists of an "anchor," which is either a trait or another node.

Say we wanted to delete the fooNode we inserted above. There are 4 ways we could specify the StableRange to delete which are all equivalent in the absence of concurrent editing:

const trait = { parent: initialTree, label: 'foo' };
const stableRange1 = StableRange.from(StablePlace.atStartOf(trait)).to(StablePlace.atEndOf(trait));
const stableRange2 = StableRange.from(StablePlace.atStartOf(trait)).to(StablePlace.after(fooNode));
const stableRange3 = StableRange.from(StablePlace.before(fooNode)).to(StablePlace.atEndOf(trait));
const stableRange4 = StableRange.from(StablePlace.before(fooNode)).to(StablePlace.after(fooNode));

Once concurrent edits are considered, the different ways to anchor this StableRange may impact whether or not this edit conflicts with others.

Also note that there are some more convenient shorthands for several of these specifications. See StableRange documentation for more information.

Status

SharedTree is in active, but still relatively early development. As such, it is lacking in some areas (such as performance testing). For an idea of some future features we'd like to support, see Future Plans.

Implementation-wise:

  • Document format may change only in major releases, and SharedTree is commited to backwards compatibility (support for older documents). For more information on this commitment, see the notes in PersistedTypes.ts.
  • APIs are not yet stable, and those beyond what's needed for the MVP (ex: history editing and inspection) are not provided yet. Core APIs are not likely to significantly change.
  • Performance is generally reasonable. However, this assessment was made using integration-style performance tests of consuming applications. Though it's on the roadmap, there are currently no rigorous performance tests which are isolated to SharedTree.

Design wise:

  • SharedTree is always created with an uninitialized state. It is up to the application to initialize the tree to something else if needed.
  • There are still open questions regarding how SharedTree will relate to the rest of the Fluid ecosystem. For example, we do not have suggested design patterns for when users of SharedTree should store references to other Fluid DataObjects versus storing the data for children as subtrees.

Edits

An Edit is the basic unit of transactionality in SharedTree. It specifies how to modify a document via a sequence of changes (see PersistedTypes.ts). Each edit, when applied to a version of the document (a Snapshot), produces a new version of the document.

Once an edit is acknowledged by the Fluid service (and thus it has a sequence number, and will be included in summaries), the version of the document it applies to is fixed: it will not be applied to any revision other than the one produced by its preceding edit. There may be operations that will create new edits based on existing ones and apply them in a different context, but these are logically considered new edits.

Conflicts

Due to the collaborative and distributed nature of SharedTree, Edits may be constructed based on a version of the tree that differs from the one they end up getting applied to. The Change API is designed to allow capturing a lot of the actual intention of edits in the Change. For example, an Insert between A and B can be anchored after A or before B. If it is really intended to be between A and B, a Constraint can be included that requires A and B are still next to each-other or the edit will conflict. It is also possible to replace the contents between A and B with the inserted content. This flexibility allows the majority of edits to be encoded in a way where their intention will be applied correctly when reordered, and in the rare cases where this can not be done, they will conflict instead of being applied in a non-intention preserving way: SharedTree generally follows this policy that it is better to fail to apply a change than to apply it in a way that violates user expectation or intention.

When a change fails to apply, or a constraint indicates that it applied, but may not have been ideal, it is called conflicted. Currently, if a change fails to apply due to a conflict, it is dropped. Improving this policy is in our future plans.

Constraints

A Constraint can be added to an Edit's list of changes which can be used to detect cases where an Edit could still apply (not-conflict) but may lose its original semantics due to reordering.

For example, two edits could be made concurrently: one that sorts a list alphabetically and one that adds an item to the list. Depending on how the sorting structures its changes and exactly where the insert occurred, the sort may or may not conflict if the insert gets acknowledged first. In some domains, it would be desired that this conflicts. In such domains, a Constraint could be added that would require the list to contain the same set of items as when the sort edit was created for it to apply correctly. The Constraint can specify what should happen if violated: see ConstraintEffect in PersistedTypes.ts for details.

Note that these constraints apply to more than just the case of edits that were made concurrently: edits to history also use conflicts (and thus constraints) to prevent historical edits from being re-contextualized in ways that break their semantics. In the above example, this could occur when a user undoes deleting an item from the list after it was sorted. If the sort has a constraint that the list contains the expected items, the undo will violate that constraint (making it not commute with the sort, even if the list was already mostly or fully sorted). This gives the application the opportunity to resolve the constraint violation by reapplying the sort on the updated list when performing the undo, and thus maintain the expected behavior that the new item (whose delete was undone) will show up sorted correctly. See the "Editing History" section below for details on how this works.

Change Rejection

In scenarios where concurrent changes occur, it is possible that the order in which the Fluid service acknowledges these changes causes a change to become invalid. Edits are transactional so any invalid change in an edit will cause the entire edit to be invalid.

However, no combination of changes will cause the client to crash. Changes go through validation before they are applied and invalid changes are currently dropped.

Example

Assuming a tree with a single node, A, a client creates an edit, 1, that inserts a node after node A. At the same time another client creates an edit, 2, that deletes node A. If the Fluid service sequences edit 2 before edit 1, edit will then becomes invalid because its anchor has been deleted. In this situation, edit 1 is dropped.

Possible Change/Edit Results

AppliedThe edit or change was applied.
Invalid ChangeA well-formed (not malformed) change which cannot be applied given the current tree and detached state.
Invalid EditA well-formed edit which cannot be applied to the current tree.
Malformed ChangeA change which can not possibly be applied to any tree without error. For example, a StablePlace with no sibling and also no trait.
Malformed EditAn edit which contains one or more malformed changes, or an edit with a sequence of changes that could not possibly be applied sequentially without error. (e.g. parent a detached node twice).

Undo

Undo in a collaborative context is complex since the change being undone may not be the most recent change. This means undo and redo really need to be treated as arbitrary history edits, adding and removing changes as specific points in the past, and reconciling the impact of that with the edits after it.

2.0.0-rc.1.0.1

4 months ago

2.0.0-rc.1.0.0

4 months ago

1.3.7

9 months ago

1.3.6

1 year ago

1.3.5-121405

1 year ago

1.3.5

1 year ago

1.3.4

1 year ago

1.2.8

1 year ago

1.4.0-121020

1 year ago

1.4.0-111997

1 year ago

1.3.3

1 year ago

1.3.1

2 years ago

1.3.0

2 years ago

1.4.0-115997

1 year ago

1.3.2-112998

1 year ago

1.4.0-108057

2 years ago

1.4.0-106438

2 years ago

1.3.0-97515

2 years ago

1.3.0-100520

2 years ago

1.2.7

2 years ago

0.59.4003

2 years ago

1.2.6-93452

2 years ago

1.2.6

2 years ago

0.59.2004

2 years ago

1.2.0-77818

2 years ago

1.2.3-83900

2 years ago

1.1.0-76254

2 years ago

1.2.0

2 years ago

1.2.5

2 years ago

1.2.4

2 years ago

1.2.3

2 years ago

1.2.2

2 years ago

1.2.1

2 years ago

1.2.0-78837

2 years ago

1.2.3-84921

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.1.2

2 years ago

1.1.0-75972

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

0.59.4002

2 years ago

0.59.2002

2 years ago

0.59.2003

2 years ago

0.59.3003

2 years ago

0.59.3001

2 years ago

0.59.3002

2 years ago

0.59.3000

2 years ago

1.0.0

2 years ago

0.59.1001

2 years ago

0.59.3000-67119

2 years ago

0.59.4000-71130

2 years ago

0.59.4000-71128

2 years ago

0.59.4000

2 years ago

0.59.4001

2 years ago

0.59.2001

2 years ago

0.59.3000-66610

2 years ago

0.59.2000

2 years ago

0.59.1001-62246

2 years ago

0.59.2000-63294

2 years ago

0.59.1000-61898

2 years ago

0.59.2000-61729

2 years ago

0.59.1000

2 years ago

0.58.3000-61081

2 years ago

0.56.11

2 years ago

0.56.10

2 years ago

0.58.2000-58133

2 years ago

0.58.2000

2 years ago

0.58.2002

2 years ago

0.58.2001

2 years ago

0.58.1000

2 years ago

0.58.1001

2 years ago

0.55.4

2 years ago

0.55.2

2 years ago

0.55.3

2 years ago

0.55.0

2 years ago

0.55.1

2 years ago

0.57.0-51086

2 years ago

0.55.0-48551

2 years ago

0.56.5

2 years ago

0.56.6

2 years ago

0.56.3

2 years ago

0.56.4

2 years ago

0.56.1

2 years ago

0.56.2

2 years ago

0.58.0-55983

2 years ago

0.56.0

2 years ago

0.54.0-47413

2 years ago

0.56.9

2 years ago

0.56.7

2 years ago

0.56.8

2 years ago

0.57.2

2 years ago

0.57.0

2 years ago

0.57.1

2 years ago

0.58.0-55561

2 years ago

0.54.3

2 years ago

0.54.1

2 years ago

0.56.0-49831

2 years ago

0.54.2

2 years ago

0.54.0

2 years ago

0.53.0

2 years ago

0.53.0-46105

2 years ago

0.52.0-44610

3 years ago

0.51.2

3 years ago

0.51.3

2 years ago

0.51.0

3 years ago

0.51.1

3 years ago

0.51.0-43124

3 years ago

0.52.1

2 years ago

0.52.0

2 years ago

0.50.3

3 years ago

0.50.4

2 years ago

0.50.2

3 years ago

0.50.1

3 years ago

0.50.0

3 years ago

0.50.0-41540

3 years ago

0.49.1

3 years ago

0.49.2

3 years ago

0.49.0

3 years ago

0.50.0-41365

3 years ago

0.48.5

3 years ago

0.49.0-39313

3 years ago

0.48.4

3 years ago

0.48.3

3 years ago

0.49.0-39015

3 years ago

0.48.2

3 years ago

0.48.1

3 years ago

0.48.0

3 years ago

0.48.0-38142

3 years ago

0.48.0-38105

3 years ago

0.46.2

3 years ago

0.47.1

3 years ago

0.47.0-36699

3 years ago

0.47.0

3 years ago

0.47.0-36362

3 years ago

0.46.1

3 years ago

0.47.0-35912

3 years ago

0.47.0-35961

3 years ago

0.46.0

3 years ago

0.46.0-34784

3 years ago

0.45.4

3 years ago

0.45.3

3 years ago

0.45.2

3 years ago

0.45.1

3 years ago

0.45.0-32948

3 years ago

0.45.0

3 years ago

0.44.1

3 years ago

0.39.8

3 years ago

0.44.0

3 years ago

0.39.7

3 years ago

0.44.0-30858

3 years ago

0.42.4

3 years ago

0.43.1

3 years ago

0.43.0

3 years ago

0.42.2

3 years ago

0.42.3

3 years ago

0.42.1

3 years ago

0.39.6

3 years ago

0.39.5

3 years ago

0.42.0

3 years ago

0.42.0-28410

3 years ago

0.41.3

3 years ago

0.41.4

3 years ago

0.41.2

3 years ago

0.42.0-27644

3 years ago

0.41.1

3 years ago

0.40.3

3 years ago

0.42.0-27549

3 years ago

0.42.0-27677

3 years ago

0.42.0-27683

3 years ago

0.41.0

3 years ago

0.41.0-27154

3 years ago

0.40.2

3 years ago

0.40.0

3 years ago

0.40.1

3 years ago

0.39.4

3 years ago

0.41.0-25957

3 years ago

0.38.2

3 years ago

0.38.1

3 years ago

0.38.4

3 years ago

0.38.3

3 years ago

0.39.0-23272

3 years ago

0.40.0-25719

3 years ago

0.39.1

3 years ago

0.39.0

3 years ago

0.39.3

3 years ago

0.39.2

3 years ago

0.40.0-25851

3 years ago

0.39.0-23254

3 years ago

0.38.0

3 years ago

0.38.0-22040

3 years ago

0.38.0-21934

3 years ago

0.38.0-22000

3 years ago

0.37.4

3 years ago

0.37.3-21287

3 years ago

0.37.3

3 years ago

0.37.2

3 years ago

0.37.1

3 years ago

0.37.0

3 years ago

0.37.0-20517

3 years ago

0.37.0-20427

3 years ago

0.35.7-20343

3 years ago

0.36.2-20364

3 years ago

0.35.5

3 years ago

0.36.1

3 years ago

0.35.6

3 years ago

0.36.0-18883

3 years ago

0.36.0

3 years ago

0.35.4

3 years ago

0.33.5

3 years ago

0.35.3

3 years ago

0.34.3

3 years ago

0.33.4

3 years ago

0.34.2

3 years ago

0.35.2

3 years ago

0.35.1

3 years ago

0.35.0

3 years ago

0.35.0-16887

3 years ago

0.35.0-16170

3 years ago

0.34.1

3 years ago

0.34.0

3 years ago

0.34.0-14942

3 years ago

0.33.3

3 years ago

0.33.2

3 years ago

0.33.1

3 years ago

0.33.0

3 years ago

0.33.0-13708

3 years ago