style(docs): Format docs content with prettier (#23118)

Also makes some line-wrapping adjustments in accordance with [our
guidelines](https://github.com/microsoft/FluidFramework/wiki/Markdown-Best-Practices#line-breaks-along-sentence-boundaries).

I discovered while auditing the prettier output that prettier does not
seem to be able to handle some aspects of JSX syntax in .mdx files.
Specifically, comments. I've filed
[AB#24430](https://dev.azure.com/fluidframework/internal/_workitems/edit/24430)
to track investigations to make this work correctly.
This commit is contained in:
Joshua Smithrud 2024-11-18 11:06:25 -08:00 коммит произвёл GitHub
Родитель 53dd76c2c5
Коммит 233bcc4c08
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
58 изменённых файлов: 1017 добавлений и 959 удалений

Просмотреть файл

@ -80,15 +80,16 @@ packages/framework/data-object-base/es5
# Generated by policy-check
packages/runtime/test-runtime-utils/src/assertionShortCodesMap.ts
# TODO: Remove these *after* merging new site infra into main, to ensure docs diff is intelligable during PR review.
docs/docs/*
docs/versioned_docs/*
# TODO: Investigate formatting options that support JSX syntax in .mdx files
docs/**/*.mdx
# Generated
docs/.doc-models
docs/.docusaurus
docs/build
docs/versions.json
docs/docs/api/*
docs/versioned_docs/*/api/*
# Formatting gets clobbered by swa
docs/swa-cli.config.json

Просмотреть файл

@ -10,6 +10,7 @@ To get started with the Fluid Framework, you'll want to take a dependency on our
You'll then want to pair that with the appropriate service client implementation based on your service.
These include:
- [@fluidframework/azure-client](./azure-client.md) (for use with [Azure Fluid Relay](../deployment/azure-frs))
- [@fluidframework/odsp-client](./odsp-client.md) (for use with [SharePoint Embedded](../deployment/sharepoint-embedded))
- [@fluidframework/tinylicious-client](./tinylicious-client) (for testing with [Tinylicious](../testing/tinylicious))

9
docs/docs/build/audience.mdx поставляемый
Просмотреть файл

@ -12,8 +12,7 @@ This document will explain how to use the audience APIs and then provide example
When creating a container, your app is also provided a container services object which holds the audience. This audience is backed by that same container. The following is an example. Note that `client` is an object of a type that is provided by a service-specific client library.
```js
const { container, services } =
await client.createContainer(containerSchema);
const { container, services } = await client.createContainer(containerSchema);
const audience = services.audience;
```
@ -28,7 +27,10 @@ export interface IMember {
}
```
An `IMember` represents a single user identity. `IMember` holds a list of `IConnection` objects, which represent that audience member's active connections to the container. Typically a user will only have one connection, but scenarios such as loading the container in multiple web contexts or on multiple computers will also result in as many connections. An audience member will always have at least one connection. Each user and each connection will both have a unique identifier.
An `IMember` represents a single user identity.
`IMember` holds a list of `IConnection` objects, which represent that audience member's active connections to the container.
Typically a user will only have one connection, but scenarios such as loading the container in multiple web contexts or on multiple computers will also result in as many connections.
An audience member will always have at least one connection. Each user and each connection will both have a unique identifier.
:::tip
@ -38,7 +40,6 @@ Connections can be short-lived and are not reused. A client that disconnects fro
### Service-specific audience data
The `ServiceAudience` class represents the base audience implementation, and individual Fluid services are expected to extend this class for their needs. Typically this is through extending `IMember` to provide richer user information and then extending `ServiceAudience` to use the `IMember` extension. For `TinyliciousAudience`, this is the only change, and it defines a `TinyliciousMember` to add a user name.
```typescript

68
docs/docs/build/container-states-events.mdx поставляемый
Просмотреть файл

@ -3,14 +3,14 @@ title: Container states and events
sidebar_position: 3
---
import { ApiLink } from "@site/src/components/shortLinks"
import { ApiLink } from "@site/src/components/shortLinks";
This article provides a detailed description of the lifecycle states of the containers and container events. It assumes that you are familiar with [Containers](./containers).
:::note
In this article the term "creating client" refers to the client on which a container is created.
When it is important to emphasize that a client is *not* the creating client, it is called a "subsequent client".
When it is important to emphasize that a client is _not_ the creating client, it is called a "subsequent client".
It is helpful to remember that "client" does not refer to a device or anything that persists between sessions with your application.
When a user closes your application, the client no longer exists: a new session is a new client.
So, when the creating client is closed, there is no longer any creating client.
@ -46,8 +46,10 @@ There are four types of states that a container can be in. Every container is in
### Publication status states
Publication status refers to whether or not the container has been *initially* saved to the Fluid service, and whether it is still persisted there.
Think of it as a mainly *service-relative* state because it is primarily about the container's state in the Fluid service and secondarily about its state on the creating client. The following diagram shows the possible publication states and the events that cause a state transition. Details are below the diagram.
Publication status refers to whether or not the container has been _initially_ saved to the Fluid service, and whether it is still persisted there.
Think of it as a mainly _service-relative_ state because it is primarily about the container's state in the Fluid service and secondarily about its state on the creating client.
The following diagram shows the possible publication states and the events that cause a state transition.
Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the four possible publication states](./images/PublicationStates.svg)
@ -112,9 +114,10 @@ How you handle the **publishing** (`AttachState.Attaching`) state depends on the
```typescript
// Code that runs only on a creating client.
if ((container.attachState !== AttachState.Attached)
&&
(container.attachState !== AttachState.Attaching)) {
if (
container.attachState !== AttachState.Attached &&
container.attachState !== AttachState.Attaching
) {
await container.attach();
}
```
@ -123,16 +126,20 @@ On the other hand, in scenarios where you want to block users from editing share
```typescript
// Code that runs only on a creating client.
if ((container.attachState === AttachState.Detached)
||
(container.attachState === AttachState.Attaching)) {
if (
container.attachState === AttachState.Detached ||
container.attachState === AttachState.Attaching
) {
// Disable editing.
}
```
### Synchronization status states
Synchronization status refers to whether the container's data on the client is saved to the Fluid service. It is a *client-relative state*: the container may have a different synchronization state on different clients. The following diagram shows the possible Synchronization states and the events that cause a state transition. Details are below the diagram.
Synchronization status refers to whether the container's data on the client is saved to the Fluid service.
It is a _client-relative state_: the container may have a different synchronization state on different clients.
The following diagram shows the possible Synchronization states and the events that cause a state transition.
Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the two possible Synchronization states](./images/SynchronizationStates.svg)
@ -150,7 +157,8 @@ The dotted arrow represents a boolean guard condition. It means that the contain
- **saved**: A container is in **saved** state on a client when the container has been published and the service has acknowledged all data changes made on the client.
When a new change is made to the data in a given client, the container moves to **dirty** state *in that client*. When all pending changes are acknowledged, it transitions to the **saved** state.
When a new change is made to the data in a given client, the container moves to **dirty** state _in that client_.
When all pending changes are acknowledged, it transitions to the **saved** state.
Note that a container in the **saved** state is not necessarily perfectly synchronized with the service.
There may be changes made on other clients that have been saved to the service but have not yet been relayed to this client.
@ -159,8 +167,8 @@ But if the client is disconnected while in **saved** state, it remains **saved**
Users can work with the container's data regardless of whether it is in **dirty** or **saved** state.
But there are scenarios in which your code must be aware of the container's state. For this reason, the `FluidContainer` object has a boolean `isDirty` property to specify its state.
The `container` object also supports *dirty* and *saved* events, so you can handle the transitions between states.
The container emits the *saved* event to notify the caller that all the local changes have been acknowledged by the service.
The `container` object also supports _dirty_ and _saved_ events, so you can handle the transitions between states.
The container emits the _saved_ event to notify the caller that all the local changes have been acknowledged by the service.
```typescript {linenos=inline}
container.on("saved", () => {
@ -168,7 +176,7 @@ container.on("saved", () => {
});
```
The container emits the *dirty* event to notify the caller that there are local changes that have not been acknowledged by the service yet.
The container emits the _dirty_ event to notify the caller that there are local changes that have not been acknowledged by the service yet.
```typescript {linenos=inline}
container.on("dirty", () => {
@ -193,7 +201,7 @@ function disconnectFromFluidService {
When a user is highly active, the container on a client may shift between **dirty** and **saved** states rapidly and repeatedly.
You usually do not want a handler to run every time the event is triggered, so take care to have your handler attached only in constrained circumstances.
In the example above, using `container.once` ensures that the handler is removed after it runs once. When the container reconnects, the *saved* event will not have the handler attached to it.
In the example above, using `container.once` ensures that the handler is removed after it runs once. When the container reconnects, the _saved_ event will not have the handler attached to it.
See [Connection status states](#connection-status-states) for more about connection and disconnection.
@ -201,7 +209,10 @@ See [Disposed](#disposed) for more about disposing the container object.
### Connection status states
Connection status refers to whether the container is connected to the Fluid service. It is a *client-relative state*: the container may have a different connection state on different clients. The following diagram shows the possible Connection states and the events that cause a state transition. Details are below the diagram.
Connection status refers to whether the container is connected to the Fluid service.
It is a _client-relative state_: the container may have a different connection state on different clients.
The following diagram shows the possible Connection states and the events that cause a state transition.
Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the four possible Connection states](./images/ConnectionStates.svg)
@ -218,7 +229,7 @@ A container is **disconnected** if it is not in any of the other three Connectio
Disconnection does not automatically block users from editing the shared data objects.
Changes they make are stored locally and will be sent to the Fluid service when connection is reestablished.
But these changes are *not* being synchronized with other clients while the current client is disconnected, and your application's user may not be aware of that.
But these changes are _not_ being synchronized with other clients while the current client is disconnected, and your application's user may not be aware of that.
So, you usually want to block editing if a disconnection continues for some time.
For more information, see [Managing connection and disconnection](#managing-connection-and-disconnection).
@ -229,8 +240,11 @@ For more information, see [Managing connection and disconnection](#managing-conn
In this state, the client is attempting to connect to the Fluid service, but has not yet received an acknowledgement.
A container moves into the **establishing connection** state in any of the following circumstances:
- On the creating client, your code calls the `container.attach` method. For more information, see [Publishing a container](./containers#publishing-a-container). This method publishes the container *and* connects the client to the service.
- Your code calls the client's `getContainer` method on a client that has not previously been connected. For more information, see [Connecting to a container](./containers#connecting-to-a-container).
- On the creating client, your code calls the `container.attach` method.
For more information, see [Publishing a container](./containers#publishing-a-container).
This method publishes the container _and_ connects the client to the service.
- Your code calls the client's `getContainer` method on a client that has not previously been connected.
For more information, see [Connecting to a container](./containers#connecting-to-a-container).
- The Fluid client runtime tries to reconnect following a disconnection caused by a network problem.
- Your code calls the `container.connect` method on a client that had become disconnected.
@ -255,7 +269,6 @@ The container transitions to this state automatically when it is fully caught up
There are scenarios in which you need to control the connection status of the container. To assist, the `container` object has the following APIs:
- A <ApiLink packageName="fluid-static" apiName="IFluidContainer" apiType="interface" headingId="connectionstate-propertysignature">container.connectionState</ApiLink> property of type `ConnectionState`. There are four possible values for the property:
- `Disconnected`
@ -263,8 +276,8 @@ There are scenarios in which you need to control the connection status of the co
- `CatchingUp`: In most scenarios, your code should treat this state the same as it treats the connected state. See [Examples](#examples). An exception would be when it is important that users see the very latest changes from other clients before they are allowed to make their own edits. In that case, your code should treat **catching up** like it treats the **disconnected** state.
- `Connected`
- A *disconnected* event, that fires if a network problem causes a disconnection or if the `container.disconnect` method is called.
- A *connected* event, that fires if the Fluid client runtime is able to reconnect (and any needed catching up is complete) or if the `container.connect` method is called.
- A _disconnected_ event, that fires if a network problem causes a disconnection or if the `container.disconnect` method is called.
- A _connected_ event, that fires if the Fluid client runtime is able to reconnect (and any needed catching up is complete) or if the `container.connect` method is called.
##### Examples
@ -280,7 +293,8 @@ container.on("connected", () => {
});
```
Your code can reduce unnecessary network traffic by disconnecting when a user is idle. The following example assumes that there is a `user` object that emits *idle* and *active* events.
Your code can reduce unnecessary network traffic by disconnecting when a user is idle.
The following example assumes that there is a `user` object that emits _idle_ and _active_ events.
```typescript
user.on("idle", () => {
@ -296,7 +310,9 @@ user.on("active", () => {
### Local Readiness state
Local Readiness is a *client-relative state*: the container may have a different Local Readiness state on different clients. The following diagram shows the possible Local Readiness states and the events that cause a state transition. Details are below the diagram.
Local Readiness is a _client-relative state_: the container may have a different Local Readiness state on different clients.
The following diagram shows the possible Local Readiness states and the events that cause a state transition.
Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the two possible Local Readiness states](./images/LocalReadinessStates.svg)
@ -311,7 +327,7 @@ A disposed `FluidContainer` remains disposed forever, but a new **ready** `Fluid
#### Disposed
In scenarios where a container is no longer needed on the current client, you can dispose it with a call of `container.dispose()`. Disposing removes any server connections, so the Connection status becomes **disconnected**.
There is a *disposed* event on the container object that you can handle to add custom clean up logic, such as removing registered events.
There is a _disposed_ event on the container object that you can handle to add custom clean up logic, such as removing registered events.
The following shows the basic syntax:
```typescript

23
docs/docs/build/containers.mdx поставляемый
Просмотреть файл

@ -3,8 +3,7 @@ title: Containers
sidebar_position: 2
---
import { ApiLink, PackageLink } from "@site/src/components/shortLinks"
import { ApiLink, PackageLink } from "@site/src/components/shortLinks";
The container is the primary unit of encapsulation in the Fluid Framework.
It enables a group of clients to access the same set of shared objects and co-author changes on those objects.
@ -20,7 +19,7 @@ This article explains:
:::note
In this article the term "creating client" refers to the client on which a container is created.
When it is important to emphasize that a client is *not* the creating client, it is called a "subsequent client".
When it is important to emphasize that a client is _not_ the creating client, it is called a "subsequent client".
It is helpful to remember that "client" does not refer to a device or anything that persists between sessions with your application.
When a user closes your application, the client no longer exists: a new session is a new client.
So, when the creating client is closed, there is no longer any creating client.
@ -30,7 +29,6 @@ The device is a subsequent client in all future sessions.
## Creating & connecting
Your code creates containers using APIs provided by a service-specific client library.
Each service-specific client library implements a common API for manipulating containers.
For example, the [Tinylicious library](/docs/testing/tinylicious) provides <PackageLink packageName="tinylicious-client">these APIs</PackageLink> for the Tinylicious Fluid service.
@ -39,7 +37,8 @@ These common APIs enable your code to specify what shared objects should live in
### Container schema
Your code must define a schema that represents the structure of the data within the container.
Only the data *values* are persisted in the Fluid service. The structure and data types are stored as a schema object on each client.
Only the data _values_ are persisted in the Fluid service.
The structure and data types are stored as a schema object on each client.
A schema can specify:
- Some initial shared objects that are created as soon as the container is created, and are immediately and always available to all connected clients.
@ -53,7 +52,7 @@ This example schema defines two initial objects, `layout` and `text`, and declar
const schema = {
initialObjects: {
layout: SharedMap,
text: SharedString
text: SharedString,
},
dynamicObjectTypes: [SharedCell, SharedString],
};
@ -70,8 +69,7 @@ const schema = {
},
};
const { container, services } =
await client.createContainer(schema);
const { container, services } = await client.createContainer(schema);
```
Notes:
@ -105,8 +103,7 @@ const schema = {
},
};
const { container, services } =
await client.createContainer(schema);
const { container, services } = await client.createContainer(schema);
const containerId = await container.attach();
```
@ -134,7 +131,7 @@ const { container, services } =
:::tip
This section provides only basic information about the *most important* states that a container can be in. Details about *all* container states, including state diagrams, state management, editing management, and container event handling are in [Container states and events](./container-states-events).
This section provides only basic information about the _most important_ states that a container can be in. Details about _all_ container states, including state diagrams, state management, editing management, and container event handling are in [Container states and events](./container-states-events).
:::
@ -185,13 +182,13 @@ The drawback of this approach is that when creating a container, the service con
Multiple Fluid containers can be loaded from an application or on a web page at the same time. There are two primary scenarios where an application would use multiple containers.
First, if your application loads two different experiences that have different underlying data structures. *Experience 1* may require a `SharedMap` and *Experience 2* may require a `SharedString`. To minimize the memory footprint of your application, your code can create two different container schemas and load only the schema that is needed. In this case your app has the capability of loading two different containers (two different schemas) but only loads one for a given user.
First, if your application loads two different experiences that have different underlying data structures. _Experience 1_ may require a `SharedMap` and _Experience 2_ may require a `SharedString`. To minimize the memory footprint of your application, your code can create two different container schemas and load only the schema that is needed. In this case your app has the capability of loading two different containers (two different schemas) but only loads one for a given user.
A more complex scenario involves loading two containers at once. Containers serve as a permissions boundary, so if you have cases where multiple users with different permissions are collaborating together, you may use multiple containers to ensure users have access only to what they should.
For example, consider an education application where multiple teachers collaborate with students. The students and teachers may have a shared view while the teachers may also have an additional private view on the side. In this scenario the students would be loading one container and the teachers would be loading two.
## Container services
When you create or connect to a container with `createContainer` or `getContainer`, the Fluid service will also return a service-specific *services* object.
When you create or connect to a container with `createContainer` or `getContainer`, the Fluid service will also return a service-specific _services_ object.
This object contains references to useful services you can use to build richer applications.
An example of a container service is the [Audience](./audience), which provides user information for clients that are connected to the container. See [Working with the audience](./audience#working-with-the-audience) for more information.

12
docs/docs/build/data-modeling.mdx поставляемый
Просмотреть файл

@ -10,9 +10,9 @@ that are immediately and always available to all clients; or, for more complex s
The most straightforward way to use Fluid is by defining **initial objects** that are created when the
[Fluid container][] is created, and exist for the lifetime of the container. Initial objects serve as a base
foundation for a Fluid *schema* -- a definition of the shape of the data.
foundation for a Fluid _schema_ -- a definition of the shape of the data.
Initial objects are always *connected* -- that is, they are connected to the Fluid service and are fully distributed.
Initial objects are always _connected_ -- that is, they are connected to the Fluid service and are fully distributed.
Your code can access initial objects via the `initialObjects` property on the `FluidContainer` object.
Your code must define at least one `initialObject`. In many cases one or more initial objects is sufficient to build a Fluid application.
@ -29,10 +29,10 @@ About this code note:
```typescript
const schema = {
initialObjects: {
customMap: SharedMap,
"customMap": SharedMap,
"custom-cell": SharedCell,
}
}
},
};
const { container, services } = await client.createContainer(schema);
@ -75,7 +75,7 @@ const newMap = await container.create(SharedMap); // Create a new SharedMap
:::tip
Another way to think about `initialObjects` and dynamic objects is as follows:
With `initialObjects`, you're telling Fluid both the type of the object *and* the key you'll use to later retrieve the
With `initialObjects`, you're telling Fluid both the type of the object _and_ the key you'll use to later retrieve the
object. This is statically defined, so Fluid can create the object for you and ensure it's always available via the key
your code defined.

48
docs/docs/build/dds.mdx поставляемый
Просмотреть файл

@ -3,27 +3,27 @@ title: Introducing distributed data structures
sidebar_position: 7
---
The Fluid Framework provides developers with two types of shared objects: *distributed data structures* (DDSes) and
The Fluid Framework provides developers with two types of shared objects: _distributed data structures_ (DDSes) and
Data Objects.
*Data Objects are beta and should not be used in production applications.*
_Data Objects are beta and should not be used in production applications._
DDSes are low-level data structures, while Data Objects are composed of DDSes and other shared objects. Data Objects are
used to organize DDSes into semantically meaningful groupings for your scenario, as well as
providing an API surface to your app's data. However, many Fluid applications will use only DDSes.
There are a number of shared objects built into the Fluid Framework. See [Distributed data structures](/docs/data-structures/overview) for more information.
DDSes automatically ensure that each client has access to the same state. They're called *distributed data structures*
DDSes automatically ensure that each client has access to the same state. They're called _distributed data structures_
because they are similar to data structures used commonly when programming, like strings, maps/dictionaries, and
objects, and arrays. The APIs provided by DDSes are designed to be familiar to programmers who've used these types of data
structures before. For example, the [SharedMap][] DDS is used to store key/value pairs, like a typical map or dictionary
data structure, and provides `get` and `set` methods to store and retrieve data in the map.
When using a DDS, you can largely treat it as a local object. Your code can add data to it, remove data, update it, etc.
However, a DDS is not *just* a local object. A DDS can also be changed by other users that are editing.
However, a DDS is not _just_ a local object. A DDS can also be changed by other users that are editing.
:::tip
Most distributed data structures are prefixed with "Shared" by convention. *SharedMap*, *SharedTree*, etc. This prefix indicates that the object is shared between multiple clients.
Most distributed data structures are prefixed with "Shared" by convention. _SharedMap_, _SharedTree_, etc. This prefix indicates that the object is shared between multiple clients.
:::
@ -38,7 +38,7 @@ Understanding the merge logic enables you to "preserve user intent" when users a
that the merge behavior should match what users intend or expect as they are editing data.
In Fluid, the merge behavior is defined by the DDS. The simplest merge strategy, employed by key-value distributed data
structures like SharedMap, is *last writer wins* (LWW). With this merge strategy, when multiple clients write different
structures like SharedMap, is _last writer wins_ (LWW). With this merge strategy, when multiple clients write different
values to the same key, the value that was written last will overwrite the others. Refer to the
[documentation for each DDS](/docs/data-structures/overview) for more details about the merge
strategy it uses.
@ -46,18 +46,18 @@ strategy it uses.
## Performance characteristics
Fluid DDSes exhibit different performance characteristics based on how they interact with the Fluid service. The DDSes
generally fall into two broad categories: *optimistic* and *consensus-based*.
generally fall into two broad categories: _optimistic_ and _consensus-based_.
:::note[See also]
* [Fluid Framework architecture](../concepts/architecture)
- [Fluid Framework architecture](../concepts/architecture)
:::
### Optimistic data structures
Optimistic DDSes apply Fluid operations locally before they are sequenced by the Fluid service.
The local changes are said to be applied *optimistically* in that they are applied **before** receiving confirmation from the Fluid service, hence the name *optimistic DDSes*.
The local changes are said to be applied _optimistically_ in that they are applied **before** receiving confirmation from the Fluid service, hence the name _optimistic DDSes_.
The benefit to this approach is the user-perceived performance; operations made by the user are reflected immediately.
The potential down-side to this approach is consistency; if another collaborator makes a concurrent edit that conflicts with, the DDS's merge resolution might end up changing the user's action after the fact.
@ -84,10 +84,10 @@ To understand why consensus-based DDSes are useful, consider implementing a stac
know!) to implement a stack DDS as an optimistic one. In the ops-based Fluid architecture, one would define an operation
like `pop`, and when a client sees that operation in the op stream, it pops a value from its local stack object.
Imagine that client A pops, and client B also pops shortly after that, but *before* it sees client A's remote pop
Imagine that client A pops, and client B also pops shortly after that, but _before_ it sees client A's remote pop
operation. With an optimistic DDS, the client will apply the local operation before the server even sees it. It doesn't
wait. Thus, client A pops a value off the local stack, and client B pops the same value -- even though it was *supposed*
to pop the second value. This represents divergent behavior; we expect a *distributed* stack to ensure that `pop`
wait. Thus, client A pops a value off the local stack, and client B pops the same value -- even though it was _supposed_
to pop the second value. This represents divergent behavior; we expect a _distributed_ stack to ensure that `pop`
operations -- and any other operation for that matter -- are applied such that the clients reach a consistent state
eventually. The optimistic implementation we just described violates that expectation.
@ -98,8 +98,8 @@ results in consistent behavior across all remote clients.
### Storing a DDS within another DDS
Distributed data structures can store primitive values like numbers and strings, and *JSON serializable* objects. For
objects that are not JSON-serializable, like DDSes, Fluid provides a mechanism called *handles*, which *are*
Distributed data structures can store primitive values like numbers and strings, and _JSON serializable_ objects. For
objects that are not JSON-serializable, like DDSes, Fluid provides a mechanism called _handles_, which _are_
serializable.
When storing a DDS within another DDS, your code must store its handle, not the DDS itself. For examples of how to do this,
@ -131,8 +131,8 @@ myMap.on("valueChanged", () => {
Because distributed data structures can be stored within each other, you can combine DDSes to create collaborative data
models. The following two questions can help determine the best data structures to use for a collaborative data model.
* What is the *granularity of collaboration* that my scenario needs?
* How does the merge behavior of a distributed data structure affect this?
- What is the _granularity of collaboration_ that my scenario needs?
- How does the merge behavior of a distributed data structure affect this?
In your scenario, what do users need to individually edit? For example, imagine that your app is a collaborative editing tool and it is storing data about
geometric shapes. The app might store the coordinates of the shape, its length, width, etc.
@ -151,7 +151,7 @@ Let's assume for a moment that all of the data about a shape is stored as a sing
}
```
If we want to make this data collaborative using Fluid, the most direct -- *but ultimately flawed* -- approach is to
If we want to make this data collaborative using Fluid, the most direct -- _but ultimately flawed_ -- approach is to
store our shape object in a SharedMap. Our SharedMap would look something like this:
```json
@ -170,7 +170,7 @@ editing the data at the same time, then the one who made the most recent change
other user.
Imagine that a user "A" is collaborating with a colleague, and the user changes the shape's width while the colleague "B" changes the
shape's height. This will generate two operations: a `set` operation for user A's change, and another `set` operation for user B's change. Both operations will be sequenced by the Fluid service, but only one will 'win,' because the SharedMap's merge behavior is LWW. Because the shape is stored as an object, both `set` operations *set the whole object*.
shape's height. This will generate two operations: a `set` operation for user A's change, and another `set` operation for user B's change. Both operations will be sequenced by the Fluid service, but only one will 'win,' because the SharedMap's merge behavior is LWW. Because the shape is stored as an object, both `set` operations _set the whole object_.
This results in someone's changes being "lost" from a user's perspective. This may be perfectly fine for your needs.
However, if your scenario requires users to edit individual properties of the shape, then the SharedMap LWW merge
@ -187,26 +187,26 @@ store the `SharedMaps` representing each shape within that parent `SharedMap` ob
In version 2.0, there's a better, way. Store a shape as an object node of a `SharedTree`. Your code can store the length in one property of the object node, the width in another, etc. Again, users can change individual properties of the shape without overwriting other users' changes.
When you have more than one shape in your data model, you could create a *array* node in the `SharedTree`, with child object nodes to store all the shapes.
When you have more than one shape in your data model, you could create a _array_ node in the `SharedTree`, with child object nodes to store all the shapes.
### Key-value data
These DDSes are used for storing key-value data. They are all optimistic and use a last-writer-wins merge policy.
* [SharedMap][] -- a basic key-value distributed data structure.
* Map nodes in a [SharedTree][] -- a hierarchical data structure with three kinds of complex nodes; maps (similar to [SharedMap][]), arrays, and JavaScript objects. There are also several kinds of leaf nodes, including boolean, string, number, null, and [Fluid handles](../concepts/handles).
- [SharedMap][] -- a basic key-value distributed data structure.
- Map nodes in a [SharedTree][] -- a hierarchical data structure with three kinds of complex nodes; maps (similar to [SharedMap][]), arrays, and JavaScript objects. There are also several kinds of leaf nodes, including boolean, string, number, null, and [Fluid handles](../concepts/handles).
### Array-like data
* Array nodes in a [SharedTree][] -- a hierarchical data structure with three kinds of complex nodes; maps (similar to [SharedMap][]), arrays, and JavaScript objects. There are also several kinds of leaf nodes, including boolean, string, number, null, and [Fluid handles](../concepts/handles).
- Array nodes in a [SharedTree][] -- a hierarchical data structure with three kinds of complex nodes; maps (similar to [SharedMap][]), arrays, and JavaScript objects. There are also several kinds of leaf nodes, including boolean, string, number, null, and [Fluid handles](../concepts/handles).
### Object data
* Object nodes in a [SharedTree][] -- a hierarchical data structure with three kinds of complex nodes; maps (similar to [SharedMap][]), arrays, and JavaScript objects. There are also several kinds of leaf nodes, including boolean, string, number, null, and [Fluid handles](../concepts/handles).
- Object nodes in a [SharedTree][] -- a hierarchical data structure with three kinds of complex nodes; maps (similar to [SharedMap][]), arrays, and JavaScript objects. There are also several kinds of leaf nodes, including boolean, string, number, null, and [Fluid handles](../concepts/handles).
### Specialized data structures
* [SharedString][] -- a specialized data structure for handling collaborative text.
- [SharedString][] -- a specialized data structure for handling collaborative text.
{/* Links */}

2
docs/docs/build/experimental-features.mdx поставляемый
Просмотреть файл

@ -33,7 +33,7 @@ The following is an example of how to enable experimental features with `AzureCl
```typescript
const configProvider = (settings: Record<string, ConfigTypes>): IConfigProviderBase => ({
getRawConfig: (name: string): ConfigTypes => settings[name]
getRawConfig: (name: string): ConfigTypes => settings[name],
});
```

6
docs/docs/build/overview.mdx поставляемый
Просмотреть файл

@ -5,7 +5,7 @@ sidebar_position: 1
:::note
This article assumes that you are familiar with the concept of *operation* in the Fluid Framework. See [How Fluid works](..#how-fluid-works).
This article assumes that you are familiar with the concept of _operation_ in the Fluid Framework. See [How Fluid works](..#how-fluid-works).
:::
@ -44,11 +44,11 @@ For more about containers see [Containers](./containers).
### Shared objects
A *shared object* is any object type that supports collaboration (simultaneous editing).
A _shared object_ is any object type that supports collaboration (simultaneous editing).
The fundamental type of shared object that is provided by Fluid Framework is called a **Distributed Data Structure (DDS)**. A DDS holds shared data that the collaborators are working with.
Fluid Framework supports a second type of shared object called **Data Object**.
*This type of object is in beta and should not be used in a production application.*
_This type of object is in beta and should not be used in a production application._
A Data Object contains one or more DDSes that are organized to enable a particular collaborative use case.
DDSes are low-level data structures, while Data Objects are composed of DDSes and other shared objects.
Data Objects are used to organize DDSes into semantically meaningful groupings for your scenario, as well as providing an API surface to your app's data.

6
docs/docs/build/releases-and-apitags.mdx поставляемый
Просмотреть файл

@ -44,20 +44,22 @@ For packages that are part of the `@fluidframework` scope and the `fluid-framewo
- For JavaScript users, such APIs can be identified via the `@beta` tag included in their documentation.
- **Legacy APIs** (`/legacy` import path) - These APIs were used by the early adopters of Fluid Framework, and we strongly discourage new applications from using these APIs.
We will continue to support use of SharedMap & SharedDirectory DDSes until we provide a migration path to SharedTree in a future release.
- For existing users of these Legacy APIs, you will have to use the /legacy import path.
This is intentional to highlight that we do not encourage new development using these APIs and plan to provide a graceful path away from them in future.
- For example, SharedMap is now a Legacy API and should be imported as follows:
```typescript
import { SharedMap } from "fluid-framework/legacy"
import { SharedMap } from "fluid-framework/legacy";
```
- For JavaScript users, such APIs can be identified via the `@legacy` tag included in their documentation.
- **System APIs** - These APIs are reserved for internal system use and are not meant to be used directly.
These may change at any time without notice.
For cases in which such a type must be referenced, the contents should never be inspected.
- For JavaScript users, such APIs can be identified via the `@system` tag included in their documentation.
- **Internal APIs** - These APIs are *strictly* for internal system use and should not be used.
- **Internal APIs** - These APIs are _strictly_ for internal system use and should not be used.
These may change at any time without notice.
- Do not import any APIs from the `/internal` import path.
- For JavaScript users, such APIs can be identified via the `@internal` tag included in their documentation.

Просмотреть файл

@ -3,8 +3,8 @@ title: Architecture
sidebar_position: 1
---
The Fluid Framework can be broken into three broad parts: The *Fluid loader*, *Fluid containers*, and the *Fluid
service*. While each of these is covered in more detail elsewhere, we'll use this space to explain the areas at a high
The Fluid Framework can be broken into three broad parts: The _Fluid loader_, _Fluid containers_, and the _Fluid
service_. While each of these is covered in more detail elsewhere, we'll use this space to explain the areas at a high
level, identify the important lower level concepts, and discuss some of our key design decisions.
## Introduction
@ -56,9 +56,10 @@ objects and distributed data structures.
If you want to load a Fluid container on your app or website, you'll load the container with the Fluid loader. If you
want to create a new collaborative experience using the Fluid Framework, you'll create a Fluid container.
A Fluid container includes state and app logic. It's a serverless app model with data persistence. It has at least one
*shared object*, which encapsulates app logic. Shared objects can have state, which is managed by *distributed data
structures* (DDSes).
A Fluid container includes state and app logic.
It's a serverless app model with data persistence.
It has at least one _shared object_, which encapsulates app logic.
Shared objects can have state, which is managed by _distributed data structures_ (DDSes).
DDSes are used to distribute state to clients. Instead of centralizing merge logic in the
server, the server passes changes (aka operations or ops) to clients and the clients perform the merge.

Просмотреть файл

@ -3,8 +3,7 @@ title: Handles
sidebar_position: 5
---
import { GlossaryLink } from "@site/src/components/shortLinks"
import { GlossaryLink } from "@site/src/components/shortLinks";
A Fluid handle is an object that holds a reference to a collaborative object, such as a <GlossaryLink term="Data object">DataObject</GlossaryLink> or a <GlossaryLink term="distributed data structures">distributed data structure</GlossaryLink> (DDS).
@ -15,12 +14,9 @@ This section covers how to consume and use Fluid handles.
- Shared objects, such as Data Objects or DDSes, cannot be stored directly in another DDS. There are two primary
reasons for this:
1. Content stored in a DDS needs to be serializable. Complex objects and classes should never be directly stored in
a DDS.
2. Frequently the same shared object (not merely a copy) has to be available in different DDSes. The only
way to make this possible is to store *references* (which is what a handle is) to the collaborative objects in
the DDSes.
1. Content stored in a DDS needs to be serializable. Complex objects and classes should never be directly stored in a DDS.
2. Frequently the same shared object (not merely a copy) has to be available in different DDSes.
The only way to make this possible is to store _references_ (which is what a handle is) to the collaborative objects in the DDSes.
- Handles encapsulate where the underlying object instance exists within the Fluid runtime and how to retrieve it.
This reduces the complexity from the caller by abstracting away the need to know how to make a `request` to the
@ -69,5 +65,5 @@ myMap2.set("my-text", myText.handle);
const text = await myMap.get("my-text").get();
const text2 = await myMap2.get("my-text").get();
console.log(text === text2) // true
console.log(text === text2); // true
```

Просмотреть файл

@ -25,7 +25,6 @@ The `Signaler` was named `SignalManager` in 1.x.
:::
### Creation
Just like with DDSes, you can include `Signaler` as a shared object you would like to load in your [FluidContainer][] schema.
@ -60,7 +59,9 @@ For more information on using `ContainerSchema` to create objects please see [Da
#### Signal request
When a client joins a collaboration session, they may need to receive information about the current state immediately after connecting the container. To support this, they can request a specific signal be sent to them from other connected clients. For example, in the [PresenceTracker](https://github.com/microsoft/FluidFramework/tree/main/examples/apps/presence-tracker) example we define a "focusRequest" signal type that a newly joining client uses to request the focus-state of each currently connected client:
When a client joins a collaboration session, they may need to receive information about the current state immediately after connecting the container.
To support this, they can request a specific signal be sent to them from other connected clients.
For example, in the [PresenceTracker](https://github.com/microsoft/FluidFramework/tree/main/examples/apps/presence-tracker) example we define a "focusRequest" signal type that a newly joining client uses to request the focus-state of each currently connected client:
```typescript
private static readonly focusRequestType = "focusRequest";
@ -80,7 +81,10 @@ this.signaler.onSignal(FocusTracker.focusRequestType, () => {
});
```
This pattern adds cost however, as it forces every connected client to generate a signal. Consider whether your scenario can be satisfied by receiving the signals naturally over time instead of requesting the information up-front. The mouse tracking in [PresenceTracker](https://github.com/microsoft/FluidFramework/tree/main/examples/apps/presence-tracker) is an example where a newly connecting client does not request current state. Since mouse movements are frequent, the newly connecting client can instead simply wait to receive other users' mouse positions on their next mousemove event.
This pattern adds cost however, as it forces every connected client to generate a signal.
Consider whether your scenario can be satisfied by receiving the signals naturally over time instead of requesting the information up-front.
The mouse tracking in [PresenceTracker](https://github.com/microsoft/FluidFramework/tree/main/examples/apps/presence-tracker) is an example where a newly connecting client does not request current state.
Since mouse movements are frequent, the newly connecting client can instead simply wait to receive other users' mouse positions on their next mousemove event.
#### Grouping signal types
@ -106,8 +110,8 @@ this.signaler.onSignal("currentlySelectedObject", (clientId, local, payload) =>
});
```
Each of the _N_ connected clients would then respond with 3 signals as well (3 _N_signals total). To bring this down to
_N_ signals total, we can group these requests into a single request that captures all the required information:
Each of the _N_ connected clients would then respond with 3 signals as well (3 _N_signals total).
To bring this down to _N_ signals total, we can group these requests into a single request that captures all the required information:
```typescript
container.on("connected", () => {

Просмотреть файл

@ -24,16 +24,15 @@ no new changes to the data structures, all clients reach an identical state in a
Fluid guarantees eventual consistency via total order broadcast. That is, when a DDS is changed locally by a client,
that change -- that is, the operation -- is first sent to the Fluid service, which does three things:
* Assigns a monotonically increasing sequence number to the operation; this is the "total order" part of total order
- Assigns a monotonically increasing sequence number to the operation; this is the "total order" part of total order
broadcast.
* Broadcasts the operation to all other connected clients; this is the "broadcast" part of total order broadcast.
* Stores the operation's data (see [data persistence](#data-persistence)).
- Broadcasts the operation to all other connected clients; this is the "broadcast" part of total order broadcast.
- Stores the operation's data (see [data persistence](#data-persistence)).
This means that each client receives every operation relayed from the server with enough information to apply them in
the correct order. The clients can then apply the operations to their local state -- which means that each client will
eventually be consistent with the client that originated the change.
## Operations
Fluid is also efficient when communicating with the server. When you change a data structure, Fluid doesn't send the

Просмотреть файл

@ -12,12 +12,16 @@ For example, in a traditional `Map`, setting a key would only set it on the loca
:::tip[Differences between Map and SharedMap]
- SharedMaps *must* use string keys.
- SharedMaps _must_ use string keys.
- You must only store the following as values in a `SharedMap`:
- *Plain objects* -- those that are safely JSON-serializable.
- _Plain objects_ -- those that are safely JSON-serializable.
If you store class instances, for example, then data synchronization will not work as expected.
- [Handles](/docs/concepts/handles) to other Fluid DDSes
- When storing objects as values in a SharedMap, changes to the object will be synchronized whole-for-whole. This means that individual changes to the properties of an object are not merged during synchronization. If you need this behavior you should store individual properties in the SharedMap instead of full objects. See [Picking the right data structure](../build/dds#picking-the-right-data-structure) for more information.
- When storing objects as values in a SharedMap, changes to the object will be synchronized whole-for-whole.
This means that individual changes to the properties of an object are not merged during synchronization.
If you need this behavior you should store individual properties in the SharedMap instead of full objects.
See [Picking the right data structure](../build/dds#picking-the-right-data-structure) for more information.
:::
For additional background on DDSes and a general overview of their design, see [Introducing distributed data structures](../build/dds).
@ -50,8 +54,8 @@ The following example loads a `SharedMap` as part of the initial roster of objec
const schema = {
initialObjects: {
customMap: SharedMap,
}
}
},
};
const { container, services } = await client.createContainer(schema);
@ -66,8 +70,8 @@ Similarly, if you are loading an existing container, the process stays largely i
const schema = {
initialObjects: {
customMap: SharedMap,
}
}
},
};
const { container, services } = await client.getContainer(id, schema);
@ -133,13 +137,13 @@ Consider the following example where you have a label and a button. When clicked
```javascript
const map = container.initialObjects.customMap;
const dataKey = "data";
const button = document.createElement('button');
const button = document.createElement("button");
button.textContent = "Randomize!";
const label = document.createElement('label');
const label = document.createElement("label");
button.addEventListener('click', () =>
button.addEventListener("click", () =>
// Set the new value on the SharedMap
map.set(dataKey, Math.random())
map.set(dataKey, Math.random()),
);
// This function will update the label from the SharedMap.
@ -149,7 +153,7 @@ const updateLabel = () => {
const value = map.get(dataKey) || 0;
label.textContent = `${value}`;
};
map.on('valueChanged', updateLabel);
map.on("valueChanged", updateLabel);
// Make sure updateLabel is called at least once.
updateLabel();
@ -162,13 +166,11 @@ Your event listener can be more sophisticated by using the additional informatio
```javascript {linenos=inline,hl_lines=["14-15"]}
const map = container.initialObjects.customMap;
const dataKey = "data";
const button = document.createElement('button');
const button = document.createElement("button");
button.textContent = "Randomize!";
const label = document.createElement('label');
const label = document.createElement("label");
button.addEventListener('click', () =>
map.set(dataKey, Math.random())
);
button.addEventListener("click", () => map.set(dataKey, Math.random()));
// Get the current value of the shared data to update the view whenever it changes.
const updateLabel = (changed, local) => {
@ -178,7 +180,7 @@ const updateLabel = (changed, local) => {
};
updateLabel(undefined, false);
// Use the changed event to trigger the rerender whenever the value changes.
map.on('valueChanged', updateLabel);
map.on("valueChanged", updateLabel);
```
Now, with the changes in `updateLabel`, the label will update to say if the value was last updated by the current user or by someone else. It will also compare the current value to the last one, and if the value has increased, it will set the text color to green. Otherwise, it will be red.
@ -230,7 +232,7 @@ And each task may also have multiple fields, including the person that it is ass
Now, the next question to ask is which of these fields you'd like to be individually collaborative. For the sake of this example, assume that the `title` and `description` are user-entered values that you'd like people to be able to edit together whereas the `assignedTo` person data is something that you receive from a backend service call that you'd like to store with your object. You can change which person the task gets assigned to but the actual metadata of each person is based off of the returned value from the backend service.
The most direct -- *but ultimately flawed* -- approach here would be to just to store the entire object into the `SharedMap` under a singular key.
The most direct -- _but ultimately flawed_ -- approach here would be to just to store the entire object into the `SharedMap` under a singular key.
This would look something like this:
@ -308,7 +310,7 @@ const pickedUser = users[pickedUserIndex];
// Now store this user object as a whole into the SharedMap
const task = map.get("task1");
map.set(task.assignedToKey, pickedUser)
map.set(task.assignedToKey, pickedUser);
```
This will work as expected **because the entire object is being stored each time** instead of specific fields.
@ -330,8 +332,8 @@ const schema = {
initialObjects: {
initialMap: SharedMap,
},
dynamicObjectTypes: [SharedMap]
}
dynamicObjectTypes: [SharedMap],
};
```
Now, you can dynamically create additional `SharedMap` instances and store their handles into the initial map that is always provided in the container.
@ -437,8 +439,8 @@ const schema = {
initialObjects: {
initialMap: SharedMap,
},
dynamicObjectTypes: [SharedMap]
}
dynamicObjectTypes: [SharedMap],
};
const { container, services } = await client.getContainer(id, schema);
@ -457,7 +459,7 @@ For example, if you wanted to fetch task with ID `task123` and allow the user to
```javascript
const taskHandle = initialMap.get("task123");
const task = await taskHandle.get();
task.set("description", editedDescription)
task.set("description", editedDescription);
```
Since each task is stored in a separate `SharedMap` and all of the fields within the task object are being stored in their own unique keys, your data now has a hierarchical structure that reflects the app's data model while individual tasks' properties can be edited independently.

Просмотреть файл

@ -22,7 +22,7 @@ Because users can simultaneously change the same DDS, you need to consider which
:::note[Meaning of "simultaneously"]
Two or more clients are said to make a change *simultaneously* if they each make a change before they have received the
Two or more clients are said to make a change _simultaneously_ if they each make a change before they have received the
others' changes from the server.
:::
@ -32,8 +32,8 @@ Choosing the correct data structure for your scenario can improve the performanc
DDSes vary from each other by three characteristics:
- **Basic data structure:** For example, key-value pair, a sequence, or a queue.
- **Client autonomy vs. Consensus:** An *optimistic* DDS enables any client to unilaterally change a value and the new
value is relayed to all other clients, while a *consensus-based* DDS will only allow a change if it is accepted by other clients via a
- **Client autonomy vs. Consensus:** An _optimistic_ DDS enables any client to unilaterally change a value and the new
value is relayed to all other clients, while a _consensus-based_ DDS will only allow a change if it is accepted by other clients via a
consensus process.
- **Merge policy:** The policy that determines how conflicting changes from clients are resolved.

Просмотреть файл

@ -32,7 +32,7 @@ Marker segments are never split or merged, and always have a length of 1.
ReferenceType.Simple,
// Arbitrary bag of properties to associate to the marker. If the marker is annotated by a future operation,
// those annotated properties will be merged with the initial set.
{ type: "pg" }
{ type: "pg" },
);
sharedString.insertText(3, "world");
// content: hi<pg marker>world
@ -49,7 +49,6 @@ Marker segments are never split or merged, and always have a length of 1.
// Handle text segments (e.g. append to the current text accumulator with `segment.text`, apply any formatting on `segment.props`)
}
});
```
<!-- It might be worth adding a section about Tile markers and their use: setting ReferenceType.Tile and putting a label on [reservedTileLabelsKey] allows

Просмотреть файл

@ -90,7 +90,7 @@ As an example, consider an app that provides a digital board with groups of stic
![A screenshot of a sticky note board app](/images/sticky-note-board-app.png)
The full sample is at: [Shared Tree Demo](https://github.com/microsoft/FluidExamples/tree/main/brainstorm). *The code snippets in this article are simplified versions of the code in the sample.*
The full sample is at: [Shared Tree Demo](https://github.com/microsoft/FluidExamples/tree/main/brainstorm). _The code snippets in this article are simplified versions of the code in the sample._
#### Object schema
@ -102,7 +102,7 @@ Use the `object()` method to create a schema for a note. Note the following abou
- The `votes` property is an array node, whose members are all strings. It is defined with an inline call of the `array()` method.
```typescript
const noteSchema = sf.object('Note', {
const noteSchema = sf.object("Note", {
id: sf.string,
text: sf.string,
author: sf.string,
@ -114,19 +114,23 @@ const noteSchema = sf.object('Note', {
Create a TypeScript datatype by extending the notional type object.
```typescript
class Note extends noteSchema { /* members of the class defined here */ };
class Note extends noteSchema {
/* members of the class defined here */
}
```
You can also make the call of the `object()` method inline as in the following:
```typescript
class Note extends sf.object('Note', {
class Note extends sf.object("Note", {
id: sf.string,
text: sf.string,
author: sf.string,
lastChanged: sf.number,
votes: sf.array(sf.string),
}) { /* members of the class defined here */ };
}) {
/* members of the class defined here */
}
```
For the remainder of this article, we use the inline style.
@ -152,7 +156,7 @@ public setColor(newColor: string) {
:::note
Do *not* override the constructor of types that you derive from objects returned by the `object()`, `array()`, and `map()` methods of `SchemaFactory`. Doing so has unexpected effects and is not supported.
Do _not_ override the constructor of types that you derive from objects returned by the `object()`, `array()`, and `map()` methods of `SchemaFactory`. Doing so has unexpected effects and is not supported.
:::
@ -171,7 +175,7 @@ class Group extends sf.object('Group', {
The app is going to need the type that is returned from `sf.array(Note)` in multiple places, including outside the context of `SchemaFactory`, so we create a TypeScript type for it as follows. Note that we include a method for adding a new note to the array of notes. The implementation is omitted, but it would wrap the constructor for the `Note` class and one or more methods in the [Array node APIs](#array-node-apis).
```typescript
class Notes extends sf.array('Notes', Note) {
class Notes extends sf.array("Notes", Note) {
public newNote(author: string) {
// implementation omitted.
}
@ -193,7 +197,7 @@ class Group extends sf.object('Group', {
As you can see from the screenshot, the top level of the root of the app's data can have two kinds of children: notes in groups and notes that are outside of any group. So, the children are defined as `Items` which is an array with two types of items. This is done by passing an array of schema types to the `array()` method. Methods for adding a new group to the app and a new note that is outside of any group are included.
```typescript
class Items extends sf.array('Items', [Group, Note]) {
class Items extends sf.array("Items", [Group, Note]) {
public newNote(author: string) {
// implementation omitted.
}
@ -207,7 +211,7 @@ class Items extends sf.array('Items', [Group, Note]) {
The root of the schema must itself have a type which is defined as follows:
```typescript
class App extends sf.object('App', {
class App extends sf.object("App", {
items: Items,
}) {}
```
@ -217,7 +221,7 @@ The final step is to create a configuration object that will be used when a `Sha
```typescript
export const appTreeConfiguration = new TreeViewConfiguration({
// root node schema
schema: App
schema: App,
});
```
@ -306,7 +310,7 @@ Leaf nodes are read and written exactly the way JavaScript primitive types are b
myNewsPaperTree.articles[1].headline = "Man bites dog";
```
The following examples show how to read from a leaf node. *Note that the datatype of `pointsForDetroitTigers` is `number`, not `sf.number`.* This is a general principle: the value returned from a leaf node, other than a `FluidHandle` node, is the underlying JavaScript primitive type.
The following examples show how to read from a leaf node. _Note that the datatype of `pointsForDetroitTigers` is `number`, not `sf.number`._ This is a general principle: the value returned from a leaf node, other than a `FluidHandle` node, is the underlying JavaScript primitive type.
```typescript
const pointsForDetroitTigers: number = seasonTree.tigersTeam.game1.points;
@ -400,7 +404,7 @@ Returns an Iterator that contains the key/value pairs in the map node. The pairs
map(callback: ()=>[]): IterableIterator<[string, T]>
```
Returns an array, *not a map node or array node*, that is the result of applying the callback parameter to each member of the original map node. It is just like [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
Returns an array, _not a map node or array node_, that is the result of applying the callback parameter to each member of the original map node. It is just like [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
##### Map node write APIs
@ -424,7 +428,7 @@ The `delete()` method removes the item with the specified key. If one client set
##### Map node properties
```typescript
size: number
size: number;
```
The total number of entries in the map node.
@ -549,7 +553,6 @@ on<K extends keyof TreeChangeEvents>(
): () => void;
```
`Tree.on` assigns the specified `listener` function to the specified `eventName` for the specified `node`.
The `node` can be any node of the tree.
The `eventName` can be either "treeChanged" or "nodeChanged".
@ -608,12 +611,11 @@ You can also pass a `TreeView` object to `runTransaction()`.
```typescript
Tree.runTransaction(myTreeView, (treeView) => {
// Make multiple changes to the tree.
})
});
```
There are example transactions here: [Shared Tree Demo](https://github.com/microsoft/FluidExamples/tree/main/brainstorm).
### Node information
```typescript

Просмотреть файл

@ -3,7 +3,7 @@ title: Connect to Azure Fluid Relay
sidebar_position: 2
---
import { PackageLink } from "@site/src/components/shortLinks"
import { PackageLink } from "@site/src/components/shortLinks";
[Azure Fluid Relay](https://aka.ms/azurefluidrelay) is a cloud-hosted Fluid service.
You can connect your Fluid application to an Azure Fluid Relay instance using the `AzureClient` in the <PackageLink packageName="azure-client">@fluidframework/azure-client</PackageLink> package.

Просмотреть файл

@ -3,7 +3,7 @@ title: Available Fluid services
sidebar_position: 1
---
import { PackageLink } from "@site/src/components/shortLinks"
import { PackageLink } from "@site/src/components/shortLinks";
The Fluid Framework can be used with any compatible service implementation. Some services, like Tinylicious, are intended only for testing and development, while other hosted options provide the high scalability needed for production-quality applications.

Просмотреть файл

@ -3,7 +3,7 @@ title: Frequently Asked Questions
sidebar_position: 7
---
import Browsers from '@site/src/pages/browsers.mdx';
import Browsers from "@site/src/pages/browsers.mdx";
The following are short, sometimes superficial, answers to some of the most commonly asked questions about the Fluid
Framework.
@ -21,7 +21,7 @@ The Fluid Framework was designed with performance and ease of development as top
### What is a DDS?
DDS is short for *distributed data structure*. DDSes are the foundation of the Fluid Framework. They are designed such
DDS is short for _distributed data structure_. DDSes are the foundation of the Fluid Framework. They are designed such
that the Fluid runtime is able to keep them in sync across clients while each client operates on the DDSes in largely
the same way they would operate on local data. The data source for a Fluid solution can represent numerous DDSes.
@ -44,7 +44,7 @@ sessions later and for efficiencies when saving to persistent storage.
**Persistent storage** is a record of ops (and summary ops) saved outside of the Fluid service. This could be a
database, blob storage, or a file. Using persistent storage allows a Fluid solution to persist across sessions.
For example, current Microsoft 365 Fluid experiences save ops in *.fluid* files in SharePoint and OneDrive.
For example, current Microsoft 365 Fluid experiences save ops in _.fluid_ files in SharePoint and OneDrive.
It is important to note that these files share many of the properties of a normal file such as permissions and a
location in a file structure, but because these experiences rely on the Fluid service, downloading the files and
working locally is not supported.
@ -194,7 +194,9 @@ Because Fluid is very client-centric, deployment is very simple.
### How does Fluid Framework deal with conflict resolution?
Conflict resolution is built into the DDSes, and the strategies vary between DDSes. For example the SharedMap uses a last-write-wins approach, whereas SharedString attempts to apply all changes while preserving user intention. The strategies used by each DDS are detailed on their respective documentation pages.
Conflict resolution is built into the DDSes, and the strategies vary between DDSes.
For example the SharedMap uses a last-write-wins approach, whereas SharedString attempts to apply all changes while preserving user intention.
The strategies used by each DDS are detailed on their respective documentation pages.
### Can we create custom strategies to handle update collisions to the distributed data structure?

Просмотреть файл

@ -5,7 +5,7 @@ sidebar_position: 8
## Attached
A Fluid container can be *attached* or *detached*. An attached container is connected to a Fluid service and can be
A Fluid container can be _attached_ or _detached_. An attached container is connected to a Fluid service and can be
loaded by other clients. Also see [Detached](#detached).
## Code loader
@ -30,7 +30,7 @@ as well as providing an API surface to your data.
## Detached
A Fluid container can be *attached* or *detached*. A detached container is not connected to a Fluid service and cannot
A Fluid container can be _attached_ or _detached_. A detached container is not connected to a Fluid service and cannot
be loaded by other clients. Newly created containers begin in a detached state, which allows developers to add initial
data if needed before attaching the container. Also see [Attached](#attached).

Просмотреть файл

@ -16,9 +16,9 @@ Because building low-latency, collaborative experiences is hard!
Fluid Framework offers:
* Client-centric application model with data persistence requiring no custom server code.
* Distributed data structures with familiar programming patterns.
* Very low latency.
- Client-centric application model with data persistence requiring no custom server code.
- Distributed data structures with familiar programming patterns.
- Very low latency.
The developers at Microsoft have built collaboration into many applications, but many required application specific
server-side logic to manage the collaborative experience. The Fluid Framework is the result of Microsoft's investment

Просмотреть файл

@ -9,13 +9,15 @@ import NodeVersions from "@site/src/pages/node-versions.mdx";
In this Quick Start you will be getting a dice roller Fluid application up and running first on your computer's
localhost.
<MockDiceRollerSample style={{
<MockDiceRollerSample
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
flexWrap: "wrap",
gap: "10px",
}} />
}}
/>
## Set up your development environment
@ -55,7 +57,6 @@ action copy the full URL in the browser, including the ID, into a new window or
second client for your dice roller application. With both windows open, click the **Roll** button in either and note
that the state of the dice changes in both clients.
🥳**Congratulations**🎉 You have successfully taken the first step towards unlocking the world of Fluid collaboration.
## Next step

Просмотреть файл

@ -38,13 +38,9 @@ A `TreeView` provides the interface for reading and editing data on the `SharedT
This is done by calling `viewWith` on the `SharedTree` with your tree configuration.
```typescript
const treeConfiguration = new TreeViewConfiguration(
{ schema: TodoList }
)
const treeConfiguration = new TreeViewConfiguration({ schema: TodoList });
const appData = container.initialObjects.appData.viewWith(
treeConfiguration
);
const appData = container.initialObjects.appData.viewWith(treeConfiguration);
```
The tree configuration takes in a schema for the root of the tree which will need to be defined by your application.
@ -59,7 +55,7 @@ The data also behaves like JS objects which makes it easy to use in external lib
To define a schema, first create a `SchemaFactory` with a unique string to use as the namespace.
```typescript
const schemaFactory = new SchemaFactory("some-schema-id-prob-a-uuid")
const schemaFactory = new SchemaFactory("some-schema-id-prob-a-uuid");
```
`SchemaFactory` provides some methods for specifying collection types including `object()`, `array()`, and `map()`; and five primitive data types for specifying leaf nodes: `boolean`, `string`, `number`, `null`, and `handle`.
@ -97,15 +93,17 @@ This can only be done once after the tree is created and the data you initialize
This is how we would initialize our todo list:
```typescript
appData.initialize(new TodoList({
appData.initialize(
new TodoList({
title: "todo list",
items: [
new TodoItem({
description: "first item",
isComplete: true,
}),
]
}));
],
}),
);
```
## Reading Data From Your Tree
@ -145,6 +143,7 @@ useEffect(() => {
```
`nodeChanged` fires whenever one or more properties of the specified node change while `treeChanged` also fires whenever any node in its subtree changes.
<ApiLink packageName="tree" apiName="TreeChangeEvents" apiType="interface">the API</ApiLink> docs for more details.
## Editing Tree Data
@ -153,10 +152,13 @@ There are built-in editing methods for each of the provided schema types.
For example, if your data is in an array, you can add a new todo item at index 3 like this:
```typescript
appData.root.items.insertAt(3, new TodoItem({
appData.root.items.insertAt(
3,
new TodoItem({
description: "new item",
isComplete: false,
}),);
}),
);
```
The schema types can also be edited using the assignment operator like this:
@ -212,10 +214,12 @@ Here is a simple example of how to do so using the provided APIs:
```typescript
const undoStack = [];
const redoStack = []
const redoStack = [];
useEffect(() => {
const unsubscribe = appData.events.on("commitApplied", (commit: CommitMetadata, getRevertible?: RevertibleFactory) => {
const unsubscribe = appData.events.on(
"commitApplied",
(commit: CommitMetadata, getRevertible?: RevertibleFactory) => {
if (getRevertible === undefined) {
return;
}
@ -232,7 +236,8 @@ useEffect(() => {
}
undoStack.push(revertible);
}
});
},
);
return unsubscribe;
}, []);
```

Просмотреть файл

@ -1,5 +1,5 @@
---
title: 'Tutorial: DiceRoller application'
title: "Tutorial: DiceRoller application"
sidebar_position: 3
---
@ -7,13 +7,15 @@ import { MockDiceRollerSample } from "@site/src/components/mockDiceRoller";
In this walkthrough, you'll learn about using the Fluid Framework by examining the DiceRoller application at https://github.com/microsoft/FluidHelloWorld. To get started, go through the [Quick Start](./quick-start) guide.
<MockDiceRollerSample style={{
<MockDiceRollerSample
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
flexWrap: "wrap",
gap: "10px",
}} />
}}
/>
<br />
In the DiceRoller app, users are shown a die with a button to roll it. When the die is rolled, the Fluid Framework syncs the data across clients so everyone sees the same result. To do this, complete the following steps:
@ -29,7 +31,7 @@ All of the work in this demo will be done in the [app.js](https://github.com/mic
Start by creating a new instance of the Tinylicious client. Tinylicious is the Fluid Framework's local testing server, and a client is responsible for creating and loading containers.
The app creates Fluid containers using a schema that defines a set of *initial objects* that will be available in the container. Learn more about initial objects in [Data modeling](../build/data-modeling).
The app creates Fluid containers using a schema that defines a set of _initial objects_ that will be available in the container. Learn more about initial objects in [Data modeling](../build/data-modeling).
Lastly, `root` defines the HTML element that the Dice will render on.
@ -71,7 +73,7 @@ const createNewDice = async () => {
dice.initialize(new Dice({ value: 1 }));
const id = await container.attach();
renderDiceRoller(dice.root, root);
}
};
```
### Loading an existing container
@ -83,7 +85,7 @@ const loadExistingDice = async (id) => {
const { container } = await client.getContainer(id, containerSchema, "2");
const dice = container.initialObjects.diceTree.viewWith(treeViewConfiguration);
renderDiceRoller(dice.root, root);
}
};
```
### Switching between loading and creating
@ -117,7 +119,6 @@ This example uses standard HTML/DOM methods to render a view.
The `renderDiceRoller` function runs only when the container is created or loaded. It appends the `diceTemplate` to the passed in HTML element, and creates a working dice roller with a random dice value each time the "Roll" button is clicked on a client.
```js
const diceTemplate = document.createElement("template");
@ -131,7 +132,7 @@ diceTemplate.innerHTML = `
<div class="dice"></div>
<button class="roll"> Roll </button>
</div>
`
`;
const renderDiceRoller = (dice, elem) => {
elem.appendChild(template.content.cloneNode(true));
@ -139,7 +140,7 @@ const renderDiceRoller = (dice, elem) => {
const diceElem = elem.querySelector(".dice");
/* REMAINDER OF THE FUNCTION IS DESCRIBED BELOW */
}
};
```
## Connect the view to Fluid data
@ -155,7 +156,7 @@ This pattern is common in Fluid because it enables the view to behave the same w
```js
rollButton.onclick = () => {
dice.value = Math.floor(Math.random() * 6) + 1;
}
};
```
### Relying on Fluid data
@ -165,7 +166,7 @@ The next line creates the function that will rerender the local view with the la
- When the container is created or loaded.
- When the dice value changes on any client.
Note that the current value is retrieved from the `SharedMap` each time `updateDice` is called. It is *not* read from the `textContent` of the local `dice` HTML element.
Note that the current value is retrieved from the `SharedMap` each time `updateDice` is called. It is _not_ read from the `textContent` of the local `dice` HTML element.
```js
const updateDice = () => {
@ -173,7 +174,7 @@ Note that the current value is retrieved from the `SharedMap` each time `updateD
// Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅)
diceElem.textContent = String.fromCodePoint(0x267f + diceValue);
diceElem.style.color = `hsl(${diceValue * 60}, 70%, 30%)`;
}
};
```
### Update on creation or load of container
@ -186,7 +187,7 @@ The next line ensures that the dice is rendered as soon as `renderDiceRoller` is
### Handling remote changes
To keep the data up to date as it changes, an event handler must be set on the `dice` object to call `updateDice` each time that the `afterChanged` event is sent. Use the built-in `Tree` object to subscribe to the event. Note that the `afterChanged` event fires whenever the `dice` object changes on *any* client; that is, when the "Roll" button is clicked on any client.
To keep the data up to date as it changes, an event handler must be set on the `dice` object to call `updateDice` each time that the `afterChanged` event is sent. Use the built-in `Tree` object to subscribe to the event. Note that the `afterChanged` event fires whenever the `dice` object changes on _any_ client; that is, when the "Roll" button is clicked on any client.
```js
Tree.on(dice, "afterChange", updateDice);

Просмотреть файл

@ -3,7 +3,7 @@ title: Logging and telemetry
sidebar_position: 2
---
import { ApiLink } from "@site/src/components/shortLinks"
import { ApiLink } from "@site/src/components/shortLinks";
Telemetry is an essential part of maintaining the health of modern applications. Fluid Framework provides a way to plug
in your own logic to handle telemetry events sent by Fluid. This enables you to integrate the Fluid telemetry along with
@ -32,8 +32,14 @@ The `Loader` constructor is called by both `createContainer()` and `getContainer
interface as its constructor argument. `ILoaderProps` interface has an optional logger parameter that will take the
`ITelemetryBaseLogger` defined by the user.
<ApiLink packageName="container-loader" apiName="ILoaderProps" apiType="interface" headingId="logger-propertysignature">ILoaderProps.logger</ApiLink>
<ApiLink
packageName="container-loader"
apiName="ILoaderProps"
apiType="interface"
headingId="logger-propertysignature"
>
ILoaderProps.logger
</ApiLink>
is used by `Loader` to pipe to container's telemetry system.
### Properties and methods
@ -116,8 +122,8 @@ export interface ITelemetryProperties {
}
export interface ITaggedTelemetryPropertyType {
value: TelemetryEventPropertyType,
tag: string,
value: TelemetryEventPropertyType;
tag: string;
}
export type TelemetryEventPropertyType = string | number | boolean | undefined;
@ -150,7 +156,7 @@ The Fluid Framework sends events in the following categories:
### EventName
This property contains a unique name for the event. The name may be namespaced, delimitted by a colon ':'.
Additionally, some event names (not the namespaces) contain underscores '_', as a free-form subdivision of
Additionally, some event names (not the namespaces) contain underscores '\_', as a free-form subdivision of
events into different related cases. Once common example is `foo_start`, `foo_end` and `foo_cancel` for
performance events.
@ -206,8 +212,10 @@ this.logger.sendTelemetryEvent({
pendingClientId: this.pendingClientId,
clientId: this.clientId,
hasTimer: this.prevClientLeftTimer.hasTimer,
inQuorum: protocolHandler !== undefined && this.pendingClientId !== undefined
&& protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
inQuorum:
protocolHandler !== undefined &&
this.pendingClientId !== undefined &&
protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
});
```
@ -260,8 +268,11 @@ async function start(): Promise<void> {
Now, whenever a telemetry event is encountered, the custom `send()` method gets called and will print out the entire
event object.
<img src="https://storage.fluidframework.com/static/images/consoleLogger_telemetry_in_action.png" alt="The
ConsoleLogger sends telemetry events to the browser console for display."/>
<img
src="https://storage.fluidframework.com/static/images/consoleLogger_telemetry_in_action.png"
alt="The
ConsoleLogger sends telemetry events to the browser console for display."
/>
:::warning
@ -280,7 +291,7 @@ in both Node.js and a web browser.
after which you will need to reload the page.
```js
localStorage.debug = 'fluid:*'
localStorage.debug = "fluid:*";
```
You'll also need to enable the `Verbose` logging level in the console. The dropdown that controls that is just above it,

Просмотреть файл

@ -46,7 +46,7 @@ The Azure Fluid Relay client can also connect to a local Tinylicious instance.
About this code note:
* The values for `tenantId`, `endpoint`, and `type` correspond to those for Tinylicious, where `7070` is the default port for Tinylicious.
- The values for `tenantId`, `endpoint`, and `type` correspond to those for Tinylicious, where `7070` is the default port for Tinylicious.
```javascript
const user = {
@ -61,7 +61,7 @@ const config = {
const clientProps = {
connection: config,
}
};
// This AzureClient instance connects to a local Tinylicious
// instance rather than a live Azure Fluid Relay
@ -79,15 +79,17 @@ The target service variable can be set as part of the test script, while secrets
```typescript
function createAzureClient(): AzureClient {
const useAzure = process.env.FLUID_CLIENT === "azure";
const tenantKey = useAzure ? process.env.FLUID_TENANTKEY as string : "";
const tenantKey = useAzure ? (process.env.FLUID_TENANTKEY as string) : "";
const user = { id: "userId", name: "Test User" };
const connectionConfig = useAzure ? {
const connectionConfig = useAzure
? {
type: "remote",
tenantId: "myTenantId",
tokenProvider: new InsecureTokenProvider(tenantKey, user),
endpoint: "https://myOrdererUrl",
} : {
}
: {
type: "local",
tokenProvider: new InsecureTokenProvider("fooBar", user),
endpoint: "http://localhost:7070",
@ -113,14 +115,13 @@ describe("ClientTest", () => {
it("can create Azure container successfully", async () => {
const schema: ContainerSchema = {
initialObjects: {
customMap: SharedMap
customMap: SharedMap,
},
};
const containerAndServices = await client.createContainer(schema);
});
});
```
You can then use the following npm scripts:

Просмотреть файл

@ -71,7 +71,7 @@ To use Tinylicious with ngrok, use the following steps. If you do not have an ng
ngrok http PORT_NUMBER
```
After completing the final step, you will see the *Forwarding URL* in your terminal, which can be used to access Tinylicious.
After completing the final step, you will see the _Forwarding URL_ in your terminal, which can be used to access Tinylicious.
Note that the ngrok URL supports both HTTP and HTTPS tunneling through to your local server.

Просмотреть файл

@ -10,6 +10,7 @@ To get started with the Fluid Framework, you'll want to take a dependency on our
You'll then want to pair that with the appropriate service client implementation based on your service.
These include:
- [@fluidframework/azure-client](./azure-client) (for use with [Azure Fluid Relay](../deployment/azure-frs))
- [@fluidframework/tinylicious-client](./tinylicious-client) (for testing with [Tinylicious](../testing/tinylicious))

Просмотреть файл

@ -12,8 +12,7 @@ This document will explain how to use the audience APIs and then provide example
When creating a container, your app is also provided a container services object which holds the audience. This audience is backed by that same container. The following is an example. Note that `client` is an object of a type that is provided by a service-specific client library.
```js
const { container, services } =
await client.createContainer(containerSchema);
const { container, services } = await client.createContainer(containerSchema);
const audience = services.audience;
```
@ -38,7 +37,6 @@ Connections can be short-lived and are not reused. A client that disconnects fro
### Service-specific audience data
The `ServiceAudience` class represents the base audience implementation, and individual Fluid services are expected to extend this class for their needs. Typically this is through extending `IMember` to provide richer user information and then extending `ServiceAudience` to use the `IMember` extension. For `TinyliciousAudience`, this is the only change, and it defines a `TinyliciousMember` to add a user name.
```typescript

Просмотреть файл

@ -3,14 +3,14 @@ title: Container states and events
sidebar_position: 3
---
import { ApiLink } from "@site/src/components/shortLinks"
import { ApiLink } from "@site/src/components/shortLinks";
This article provides a detailed description of the lifecycle states of the containers and container events. It assumes that you are familiar with [Containers](./containers).
:::note
In this article the term "creating client" refers to the client on which a container is created.
When it is important to emphasize that a client is *not* the creating client, it is called a "subsequent client".
When it is important to emphasize that a client is _not_ the creating client, it is called a "subsequent client".
It is helpful to remember that "client" does not refer to a device or anything that persists between sessions with your application.
When a user closes your application, the client no longer exists: a new session is a new client.
So, when the creating client is closed, there is no longer any creating client.
@ -46,8 +46,8 @@ There are four types of states that a container can be in. Every container is in
### Publication status states
Publication status refers to whether or not the container has been *initially* saved to the Fluid service, and whether it is still persisted there.
Think of it as a mainly *service-relative* state because it is primarily about the container's state in the Fluid service and secondarily about its state on the creating client. The following diagram shows the possible publication states and the events that cause a state transition. Details are below the diagram.
Publication status refers to whether or not the container has been _initially_ saved to the Fluid service, and whether it is still persisted there.
Think of it as a mainly _service-relative_ state because it is primarily about the container's state in the Fluid service and secondarily about its state on the creating client. The following diagram shows the possible publication states and the events that cause a state transition. Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the four possible publication states](./images/PublicationStates.svg)
@ -112,9 +112,10 @@ How you handle the **publishing** (`AttachState.Attaching`) state depends on the
```typescript
// Code that runs only on a creating client.
if ((container.attachState !== AttachState.Attached)
&&
(container.attachState !== AttachState.Attaching)) {
if (
container.attachState !== AttachState.Attached &&
container.attachState !== AttachState.Attaching
) {
await container.attach();
}
```
@ -123,16 +124,17 @@ On the other hand, in scenarios where you want to block users from editing share
```typescript
// Code that runs only on a creating client.
if ((container.attachState === AttachState.Detached)
||
(container.attachState === AttachState.Attaching)) {
if (
container.attachState === AttachState.Detached ||
container.attachState === AttachState.Attaching
) {
// Disable editing.
}
```
### Synchronization status states
Synchronization status refers to whether the container's data on the client is saved to the Fluid service. It is a *client-relative state*: the container may have a different synchronization state on different clients. The following diagram shows the possible Synchronization states and the events that cause a state transition. Details are below the diagram.
Synchronization status refers to whether the container's data on the client is saved to the Fluid service. It is a _client-relative state_: the container may have a different synchronization state on different clients. The following diagram shows the possible Synchronization states and the events that cause a state transition. Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the two possible Synchronization states](./images/SynchronizationStates.svg)
@ -150,7 +152,7 @@ The dotted arrow represents a boolean guard condition. It means that the contain
- **saved**: A container is in **saved** state on a client when the container has been published and the service has acknowledged all data changes made on the client.
When a new change is made to the data in a given client, the container moves to **dirty** state *in that client*. When all pending changes are acknowledged, it transitions to the **saved** state.
When a new change is made to the data in a given client, the container moves to **dirty** state _in that client_. When all pending changes are acknowledged, it transitions to the **saved** state.
Note that a container in the **saved** state is not necessarily perfectly synchronized with the service.
There may be changes made on other clients that have been saved to the service but have not yet been relayed to this client.
@ -159,8 +161,8 @@ But if the client is disconnected while in **saved** state, it remains **saved**
Users can work with the container's data regardless of whether it is in **dirty** or **saved** state.
But there are scenarios in which your code must be aware of the container's state. For this reason, the `FluidContainer` object has a boolean `isDirty` property to specify its state.
The `container` object also supports *dirty* and *saved* events, so you can handle the transitions between states.
The container emits the *saved* event to notify the caller that all the local changes have been acknowledged by the service.
The `container` object also supports _dirty_ and _saved_ events, so you can handle the transitions between states.
The container emits the _saved_ event to notify the caller that all the local changes have been acknowledged by the service.
```typescript {linenos=inline}
container.on("saved", () => {
@ -168,7 +170,7 @@ container.on("saved", () => {
});
```
The container emits the *dirty* event to notify the caller that there are local changes that have not been acknowledged by the service yet.
The container emits the _dirty_ event to notify the caller that there are local changes that have not been acknowledged by the service yet.
```typescript {linenos=inline}
container.on("dirty", () => {
@ -193,7 +195,7 @@ function disconnectFromFluidService {
When a user is highly active, the container on a client may shift between **dirty** and **saved** states rapidly and repeatedly.
You usually do not want a handler to run every time the event is triggered, so take care to have your handler attached only in constrained circumstances.
In the example above, using `container.once` ensures that the handler is removed after it runs once. When the container reconnects, the *saved* event will not have the handler attached to it.
In the example above, using `container.once` ensures that the handler is removed after it runs once. When the container reconnects, the _saved_ event will not have the handler attached to it.
See [Connection status states](#connection-status-states) for more about connection and disconnection.
@ -201,7 +203,7 @@ See [Disposed](#disposed) for more about disposing the container object.
### Connection status states
Connection status refers to whether the container is connected to the Fluid service. It is a *client-relative state*: the container may have a different connection state on different clients. The following diagram shows the possible Connection states and the events that cause a state transition. Details are below the diagram.
Connection status refers to whether the container is connected to the Fluid service. It is a _client-relative state_: the container may have a different connection state on different clients. The following diagram shows the possible Connection states and the events that cause a state transition. Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the four possible Connection states](./images/ConnectionStates.svg)
@ -218,7 +220,7 @@ A container is **disconnected** if it is not in any of the other three Connectio
Disconnection does not automatically block users from editing the shared data objects.
Changes they make are stored locally and will be sent to the Fluid service when connection is reestablished.
But these changes are *not* being synchronized with other clients while the current client is disconnected, and your application's user may not be aware of that.
But these changes are _not_ being synchronized with other clients while the current client is disconnected, and your application's user may not be aware of that.
So, you usually want to block editing if a disconnection continues for some time.
For more information, see [Managing connection and disconnection](#managing-connection-and-disconnection).
@ -229,7 +231,7 @@ For more information, see [Managing connection and disconnection](#managing-conn
In this state, the client is attempting to connect to the Fluid service, but has not yet received an acknowledgement.
A container moves into the **establishing connection** state in any of the following circumstances:
- On the creating client, your code calls the `container.attach` method. For more information, see [Publishing a container](./containers#publishing-a-container). This method publishes the container *and* connects the client to the service.
- On the creating client, your code calls the `container.attach` method. For more information, see [Publishing a container](./containers#publishing-a-container). This method publishes the container _and_ connects the client to the service.
- Your code calls the client's `getContainer` method on a client that has not previously been connected. For more information, see [Connecting to a container](./containers#connecting-to-a-container).
- The Fluid client runtime tries to reconnect following a disconnection caused by a network problem.
- Your code calls the `container.connect` method on a client that had become disconnected.
@ -255,7 +257,6 @@ The container transitions to this state automatically when it is fully caught up
There are scenarios in which you need to control the connection status of the container. To assist, the `container` object has the following APIs:
- A <ApiLink packageName="fluid-static" apiName="IFluidContainer" apiType="interface" headingId="connectionstate-propertysignature">container.connectionState</ApiLink> property of type `ConnectionState`. There are four possible values for the property:
- `Disconnected`
@ -263,8 +264,8 @@ There are scenarios in which you need to control the connection status of the co
- `CatchingUp`: In most scenarios, your code should treat this state the same as it treats the connected state. See [Examples](#examples). An exception would be when it is important that users see the very latest changes from other clients before they are allowed to make their own edits. In that case, your code should treat **catching up** like it treats the **disconnected** state.
- `Connected`
- A *disconnected* event, that fires if a network problem causes a disconnection or if the `container.disconnect` method is called.
- A *connected* event, that fires if the Fluid client runtime is able to reconnect (and any needed catching up is complete) or if the `container.connect` method is called.
- A _disconnected_ event, that fires if a network problem causes a disconnection or if the `container.disconnect` method is called.
- A _connected_ event, that fires if the Fluid client runtime is able to reconnect (and any needed catching up is complete) or if the `container.connect` method is called.
##### Examples
@ -280,7 +281,7 @@ container.on("connected", () => {
});
```
Your code can reduce unnecessary network traffic by disconnecting when a user is idle. The following example assumes that there is a `user` object that emits *idle* and *active* events.
Your code can reduce unnecessary network traffic by disconnecting when a user is idle. The following example assumes that there is a `user` object that emits _idle_ and _active_ events.
```typescript
user.on("idle", () => {
@ -296,7 +297,7 @@ user.on("active", () => {
### Local Readiness state
Local Readiness is a *client-relative state*: the container may have a different Local Readiness state on different clients. The following diagram shows the possible Local Readiness states and the events that cause a state transition. Details are below the diagram.
Local Readiness is a _client-relative state_: the container may have a different Local Readiness state on different clients. The following diagram shows the possible Local Readiness states and the events that cause a state transition. Details are below the diagram.
{/* TO MODIFY THIS DIAGRAM, SEE INSTRUCTIONS AT THE BOTTOM OF THIS FILE. */}
![A state diagram of the two possible Local Readiness states](./images/LocalReadinessStates.svg)
@ -311,7 +312,7 @@ A disposed `FluidContainer` remains disposed forever, but a new **ready** `Fluid
#### Disposed
In scenarios where a container is no longer needed on the current client, you can dispose it with a call of `container.dispose()`. Disposing removes any server connections, so the Connection status becomes **disconnected**.
There is a *disposed* event on the container object that you can handle to add custom clean up logic, such as removing registered events.
There is a _disposed_ event on the container object that you can handle to add custom clean up logic, such as removing registered events.
The following shows the basic syntax:
```typescript

Просмотреть файл

@ -3,8 +3,7 @@ title: Containers
sidebar_position: 2
---
import { ApiLink, PackageLink } from "@site/src/components/shortLinks"
import { ApiLink, PackageLink } from "@site/src/components/shortLinks";
The container is the primary unit of encapsulation in the Fluid Framework.
It enables a group of clients to access the same set of shared objects and co-author changes on those objects.
@ -20,7 +19,7 @@ This article explains:
:::note
In this article the term "creating client" refers to the client on which a container is created.
When it is important to emphasize that a client is *not* the creating client, it is called a "subsequent client".
When it is important to emphasize that a client is _not_ the creating client, it is called a "subsequent client".
It is helpful to remember that "client" does not refer to a device or anything that persists between sessions with your application.
When a user closes your application, the client no longer exists: a new session is a new client.
So, when the creating client is closed, there is no longer any creating client.
@ -30,7 +29,6 @@ The device is a subsequent client in all future sessions.
## Creating & connecting
Your code creates containers using APIs provided by a service-specific client library.
Each service-specific client library implements a common API for manipulating containers.
For example, the [Tinylicious library](/docs/testing/tinylicious) provides <PackageLink packageName="tinylicious-client">these APIs</PackageLink> for the Tinylicious Fluid service.
@ -39,7 +37,7 @@ These common APIs enable your code to specify what shared objects should live in
### Container schema
Your code must define a schema that represents the structure of the data within the container.
Only the data *values* are persisted in the Fluid service. The structure and data types are stored as a schema object on each client.
Only the data _values_ are persisted in the Fluid service. The structure and data types are stored as a schema object on each client.
A schema can specify:
- Some initial shared objects that are created as soon as the container is created, and are immediately and always available to all connected clients.
@ -53,7 +51,7 @@ This example schema defines two initial objects, `layout` and `text`, and declar
const schema = {
initialObjects: {
layout: SharedMap,
text: SharedString
text: SharedString,
},
dynamicObjectTypes: [SharedCell, SharedString],
};
@ -70,8 +68,7 @@ const schema = {
},
};
const { container, services } =
await client.createContainer(schema);
const { container, services } = await client.createContainer(schema);
```
Notes:
@ -105,8 +102,7 @@ const schema = {
},
};
const { container, services } =
await client.createContainer(schema);
const { container, services } = await client.createContainer(schema);
const containerId = await container.attach();
```
@ -134,7 +130,7 @@ const { container, services } =
:::tip
This section provides only basic information about the *most important* states that a container can be in. Details about *all* container states, including state diagrams, state management, editing management, and container event handling are in [Container states and events](./container-states-events).
This section provides only basic information about the _most important_ states that a container can be in. Details about _all_ container states, including state diagrams, state management, editing management, and container event handling are in [Container states and events](./container-states-events).
:::
@ -185,13 +181,13 @@ The drawback of this approach is that when creating a container, the service con
Multiple Fluid containers can be loaded from an application or on a web page at the same time. There are two primary scenarios where an application would use multiple containers.
First, if your application loads two different experiences that have different underlying data structures. *Experience 1* may require a `SharedMap` and *Experience 2* may require a `SharedString`. To minimize the memory footprint of your application, your code can create two different container schemas and load only the schema that is needed. In this case your app has the capability of loading two different containers (two different schemas) but only loads one for a given user.
First, if your application loads two different experiences that have different underlying data structures. _Experience 1_ may require a `SharedMap` and _Experience 2_ may require a `SharedString`. To minimize the memory footprint of your application, your code can create two different container schemas and load only the schema that is needed. In this case your app has the capability of loading two different containers (two different schemas) but only loads one for a given user.
A more complex scenario involves loading two containers at once. Containers serve as a permissions boundary, so if you have cases where multiple users with different permissions are collaborating together, you may use multiple containers to ensure users have access only to what they should.
For example, consider an education application where multiple teachers collaborate with students. The students and teachers may have a shared view while the teachers may also have an additional private view on the side. In this scenario the students would be loading one container and the teachers would be loading two.
## Container services
When you create or connect to a container with `createContainer` or `getContainer`, the Fluid service will also return a service-specific *services* object.
When you create or connect to a container with `createContainer` or `getContainer`, the Fluid service will also return a service-specific _services_ object.
This object contains references to useful services you can use to build richer applications.
An example of a container service is the [Audience](./audience), which provides user information for clients that are connected to the container. See [Working with the audience](./audience#working-with-the-audience) for more information.

Просмотреть файл

@ -10,9 +10,9 @@ that are immediately and always available to all clients; or, for more complex s
The most straightforward way to use Fluid is by defining **initial objects** that are created when the
[Fluid container][] is created, and exist for the lifetime of the container. Initial objects serve as a base
foundation for a Fluid *schema* -- a definition of the shape of the data.
foundation for a Fluid _schema_ -- a definition of the shape of the data.
Initial objects are always *connected* -- that is, they are connected to the Fluid service and are fully distributed.
Initial objects are always _connected_ -- that is, they are connected to the Fluid service and are fully distributed.
Your code can access initial objects via the `initialObjects` property on the `FluidContainer` object.
Your code must define at least one `initialObject`. In many cases one or more initial objects is sufficient to build a Fluid application.
@ -29,10 +29,10 @@ About this code note:
```typescript
const schema = {
initialObjects: {
customMap: SharedMap,
"customMap": SharedMap,
"custom-cell": SharedCell,
}
}
},
};
const { container, services } = await client.createContainer(schema);
@ -75,7 +75,7 @@ const newMap = await container.create(SharedMap); // Create a new SharedMap
:::tip
Another way to think about `initialObjects` and dynamic objects is as follows:
With `initialObjects`, you're telling Fluid both the type of the object *and* the key you'll use to later retrieve the
With `initialObjects`, you're telling Fluid both the type of the object _and_ the key you'll use to later retrieve the
object. This is statically defined, so Fluid can create the object for you and ensure it's always available via the key
your code defined.

Просмотреть файл

@ -3,27 +3,27 @@ title: Introducing distributed data structures
sidebar_position: 7
---
The Fluid Framework provides developers with two types of shared objects: *distributed data structures* (DDSes) and
The Fluid Framework provides developers with two types of shared objects: _distributed data structures_ (DDSes) and
Data Objects.
*Data Objects are beta and should not be used in production applications.*
_Data Objects are beta and should not be used in production applications._
DDSes are low-level data structures, while Data Objects are composed of DDSes and other shared objects. Data Objects are
used to organize DDSes into semantically meaningful groupings for your scenario, as well as
providing an API surface to your app's data. However, many Fluid applications will use only DDSes.
There are a number of shared objects built into the Fluid Framework. See [Distributed data structures](/docs/data-structures/overview) for more information.
DDSes automatically ensure that each client has access to the same state. They're called *distributed data structures*
DDSes automatically ensure that each client has access to the same state. They're called _distributed data structures_
because they are similar to data structures used commonly when programming, like strings, maps/dictionaries, and
objects, and arrays. The APIs provided by DDSes are designed to be familiar to programmers who've used these types of data
structures before. For example, the [SharedMap][] DDS is used to store key/value pairs, like a typical map or dictionary
data structure, and provides `get` and `set` methods to store and retrieve data in the map.
When using a DDS, you can largely treat it as a local object. Your code can add data to it, remove data, update it, etc.
However, a DDS is not *just* a local object. A DDS can also be changed by other users that are editing.
However, a DDS is not _just_ a local object. A DDS can also be changed by other users that are editing.
:::tip
Most distributed data structures are prefixed with "Shared" by convention. *SharedMap*, *SharedString*, etc. This prefix indicates that the object is shared between multiple clients.
Most distributed data structures are prefixed with "Shared" by convention. _SharedMap_, _SharedString_, etc. This prefix indicates that the object is shared between multiple clients.
:::
@ -38,7 +38,7 @@ Understanding the merge logic enables you to "preserve user intent" when users a
that the merge behavior should match what users intend or expect as they are editing data.
In Fluid, the merge behavior is defined by the DDS. The simplest merge strategy, employed by key-value distributed data
structures like SharedMap, is *last writer wins* (LWW). With this merge strategy, when multiple clients write different
structures like SharedMap, is _last writer wins_ (LWW). With this merge strategy, when multiple clients write different
values to the same key, the value that was written last will overwrite the others. Refer to the
[documentation for each DDS](/docs/data-structures/overview) for more details about the merge
strategy it uses.
@ -46,18 +46,18 @@ strategy it uses.
## Performance characteristics
Fluid DDSes exhibit different performance characteristics based on how they interact with the Fluid service. The DDSes
generally fall into two broad categories: *optimistic* and *consensus-based*.
generally fall into two broad categories: _optimistic_ and _consensus-based_.
:::note[See also]
* [Fluid Framework architecture](../concepts/architecture)
- [Fluid Framework architecture](../concepts/architecture)
:::
### Optimistic data structures
Optimistic DDSes apply Fluid operations locally before they are sequenced by the Fluid service.
The local changes are said to be applied *optimistically* in that they are applied **before** receiving confirmation from the Fluid service, hence the name *optimistic DDSes*.
The local changes are said to be applied _optimistically_ in that they are applied **before** receiving confirmation from the Fluid service, hence the name _optimistic DDSes_.
The benefit to this approach is the user-perceived performance; operations made by the user are reflected immediately.
The potential down-side to this approach is consistency; if another collaborator makes a concurrent edit that conflicts with, the DDS's merge resolution might end up changing the user's action after the fact.
@ -84,10 +84,10 @@ To understand why consensus-based DDSes are useful, consider implementing a stac
know!) to implement a stack DDS as an optimistic one. In the ops-based Fluid architecture, one would define an operation
like `pop`, and when a client sees that operation in the op stream, it pops a value from its local stack object.
Imagine that client A pops, and client B also pops shortly after that, but *before* it sees client A's remote pop
Imagine that client A pops, and client B also pops shortly after that, but _before_ it sees client A's remote pop
operation. With an optimistic DDS, the client will apply the local operation before the server even sees it. It doesn't
wait. Thus, client A pops a value off the local stack, and client B pops the same value -- even though it was *supposed*
to pop the second value. This represents divergent behavior; we expect a *distributed* stack to ensure that `pop`
wait. Thus, client A pops a value off the local stack, and client B pops the same value -- even though it was _supposed_
to pop the second value. This represents divergent behavior; we expect a _distributed_ stack to ensure that `pop`
operations -- and any other operation for that matter -- are applied such that the clients reach a consistent state
eventually. The optimistic implementation we just described violates that expectation.
@ -98,8 +98,8 @@ results in consistent behavior across all remote clients.
### Storing a DDS within another DDS
Distributed data structures can store primitive values like numbers and strings, and *JSON serializable* objects. For
objects that are not JSON-serializable, like DDSes, Fluid provides a mechanism called *handles*, which *are*
Distributed data structures can store primitive values like numbers and strings, and _JSON serializable_ objects. For
objects that are not JSON-serializable, like DDSes, Fluid provides a mechanism called _handles_, which _are_
serializable.
When storing a DDS within another DDS, your code must store its handle, not the DDS itself. For examples of how to do this,
@ -131,8 +131,8 @@ myMap.on("valueChanged", () => {
Because distributed data structures can be stored within each other, you can combine DDSes to create collaborative data
models. The following two questions can help determine the best data structures to use for a collaborative data model.
* What is the *granularity of collaboration* that my scenario needs?
* How does the merge behavior of a distributed data structure affect this?
- What is the _granularity of collaboration_ that my scenario needs?
- How does the merge behavior of a distributed data structure affect this?
In your scenario, what do users need to individually edit? For example, imagine that your app is a collaborative editing tool and it is storing data about
geometric shapes. The app might store the coordinates of the shape, its length, width, etc.
@ -151,7 +151,7 @@ Let's assume for a moment that all of the data about a shape is stored as a sing
}
```
If we want to make this data collaborative using Fluid, the most direct -- *but ultimately flawed* -- approach is to
If we want to make this data collaborative using Fluid, the most direct -- _but ultimately flawed_ -- approach is to
store our shape object in a SharedMap. Our SharedMap would look something like this:
```json
@ -170,7 +170,7 @@ editing the data at the same time, then the one who made the most recent change
other user.
Imagine that a user "A" is collaborating with a colleague, and the user changes the shape's width while the colleague "B" changes the
shape's height. This will generate two operations: a `set` operation for user A's change, and another `set` operation for user B's change. Both operations will be sequenced by the Fluid service, but only one will 'win,' because the SharedMap's merge behavior is LWW. Because the shape is stored as an object, both `set` operations *set the whole object*.
shape's height. This will generate two operations: a `set` operation for user A's change, and another `set` operation for user B's change. Both operations will be sequenced by the Fluid service, but only one will 'win,' because the SharedMap's merge behavior is LWW. Because the shape is stored as an object, both `set` operations _set the whole object_.
This results in someone's changes being "lost" from a user's perspective. This may be perfectly fine for your needs.
However, if your scenario requires users to edit individual properties of the shape, then the SharedMap LWW merge
@ -189,7 +189,7 @@ store the `SharedMaps` representing each shape within that parent `SharedMap` ob
In version 2.0, there's a better, way. Store a shape as an object node of a [SharedTree][]. Your code can store the length in one property of the object node, the width in another, etc. Again, users can change individual properties of the shape without overwriting other users' changes.
When you have more than one shape in your data model, you could create a *array* node in the `SharedTree`, with child object nodes to store all the shapes.
When you have more than one shape in your data model, you could create a _array_ node in the `SharedTree`, with child object nodes to store all the shapes.
:::
@ -197,12 +197,12 @@ When you have more than one shape in your data model, you could create a *array*
These DDSes are used for storing key-value data. They are all optimistic and use a last-writer-wins merge policy.
* [SharedMap][] -- a basic key-value distributed data structure.
- [SharedMap][] -- a basic key-value distributed data structure.
### Specialized data structures
* [SharedCounter][] -- a distributed counter. (Deprecated in Fluid Framework 2.0.)
* [SharedString][] -- a specialized data structure for handling collaborative text.
- [SharedCounter][] -- a distributed counter. (Deprecated in Fluid Framework 2.0.)
- [SharedString][] -- a specialized data structure for handling collaborative text.
{/* Links */}

Просмотреть файл

@ -5,7 +5,7 @@ sidebar_position: 1
:::note
This article assumes that you are familiar with the concept of *operation* in the Fluid Framework. See [How Fluid works](..#how-fluid-works).
This article assumes that you are familiar with the concept of _operation_ in the Fluid Framework. See [How Fluid works](..#how-fluid-works).
:::
@ -40,11 +40,11 @@ For more about containers see [Containers](./containers).
### Shared objects
A *shared object* is any object type that supports collaboration (simultaneous editing).
A _shared object_ is any object type that supports collaboration (simultaneous editing).
The fundamental type of shared object that is provided by Fluid Framework is called a **Distributed Data Structure (DDS)**. A DDS holds shared data that the collaborators are working with.
Fluid Framework supports a second type of shared object called **Data Object**.
*This type of object is in beta and should not be used in a production application.*
_This type of object is in beta and should not be used in a production application._
A Data Object contains one or more DDSes that are organized to enable a particular collaborative use case.
DDSes are low-level data structures, while Data Objects are composed of DDSes and other shared objects.
Data Objects are used to organize DDSes into semantically meaningful groupings for your scenario, as well as providing an API surface to your app's data.

Просмотреть файл

@ -3,8 +3,8 @@ title: Architecture
sidebar_position: 1
---
The Fluid Framework can be broken into three broad parts: The *Fluid loader*, *Fluid containers*, and the *Fluid
service*. While each of these is covered in more detail elsewhere, we'll use this space to explain the areas at a high
The Fluid Framework can be broken into three broad parts: The _Fluid loader_, _Fluid containers_, and the _Fluid
service_. While each of these is covered in more detail elsewhere, we'll use this space to explain the areas at a high
level, identify the important lower level concepts, and discuss some of our key design decisions.
## Introduction
@ -57,8 +57,8 @@ If you want to load a Fluid container on your app or website, you'll load the co
want to create a new collaborative experience using the Fluid Framework, you'll create a Fluid container.
A Fluid container includes state and app logic. It's a serverless app model with data persistence. It has at least one
*shared object*, which encapsulates app logic. Shared objects can have state, which is managed by *distributed data
structures* (DDSes).
_shared object_, which encapsulates app logic. Shared objects can have state, which is managed by _distributed data
structures_ (DDSes).
DDSes are used to distribute state to clients. Instead of centralizing merge logic in the
server, the server passes changes (aka operations or ops) to clients and the clients perform the merge.

Просмотреть файл

@ -3,8 +3,7 @@ title: Handles
sidebar_position: 5
---
import { GlossaryLink } from "@site/src/components/shortLinks"
import { GlossaryLink } from "@site/src/components/shortLinks";
A Fluid handle is an object that holds a reference to a collaborative object, such as a <GlossaryLink term="Data object">DataObject</GlossaryLink> or a <GlossaryLink term="distributed data structures">distributed data structure</GlossaryLink> (DDS).
@ -19,7 +18,7 @@ This section covers how to consume and use Fluid handles.
1. Content stored in a DDS needs to be serializable. Complex objects and classes should never be directly stored in
a DDS.
2. Frequently the same shared object (not merely a copy) has to be available in different DDSes. The only
way to make this possible is to store *references* (which is what a handle is) to the collaborative objects in
way to make this possible is to store _references_ (which is what a handle is) to the collaborative objects in
the DDSes.
- Handles encapsulate where the underlying object instance exists within the Fluid runtime and how to retrieve it.
@ -69,5 +68,5 @@ myMap2.set("my-text", myText.handle);
const text = await myMap.get("my-text").get();
const text2 = await myMap2.get("my-text").get();
console.log(text === text2) // true
console.log(text === text2); // true
```

Просмотреть файл

@ -25,7 +25,6 @@ The `Signaler` was named `SignalManager` in 1.x.
:::
### Creation
Just like with DDSes, you can include `Signaler` as a shared object you would like to load in your [FluidContainer][] schema.
@ -107,7 +106,7 @@ this.signaler.onSignal("currentlySelectedObject", (clientId, local, payload) =>
```
Each of the _N_ connected clients would then respond with 3 signals as well (3 _N_signals total). To bring this down to
_N_ signals total, we can group these requests into a single request that captures all the required information:
\_N_ signals total, we can group these requests into a single request that captures all the required information:
```typescript
container.on("connected", () => {

Просмотреть файл

@ -24,16 +24,15 @@ no new changes to the data structures, all clients reach an identical state in a
Fluid guarantees eventual consistency via total order broadcast. That is, when a DDS is changed locally by a client,
that change -- that is, the operation -- is first sent to the Fluid service, which does three things:
* Assigns a monotonically increasing sequence number to the operation; this is the "total order" part of total order
- Assigns a monotonically increasing sequence number to the operation; this is the "total order" part of total order
broadcast.
* Broadcasts the operation to all other connected clients; this is the "broadcast" part of total order broadcast.
* Stores the operation's data (see [data persistence](#data-persistence)).
- Broadcasts the operation to all other connected clients; this is the "broadcast" part of total order broadcast.
- Stores the operation's data (see [data persistence](#data-persistence)).
This means that each client receives every operation relayed from the server with enough information to apply them in
the correct order. The clients can then apply the operations to their local state -- which means that each client will
eventually be consistent with the client that originated the change.
## Operations
Fluid is also efficient when communicating with the server. When you change a data structure, Fluid doesn't send the

Просмотреть файл

@ -158,6 +158,7 @@ For a comprehensive view of the `counter` package's API documentation, see [the
<!-- Links -->
<!-- TODO: use ApiLink -->
[increment]: ../api/counter/isharedcounter-interface#increment-methodsignature
[incremented]: ../api/counter/isharedcounterevents-interface#_call_-callsignature
[Optimistic DDS]: ../build/dds#optimistic-data-structures

Просмотреть файл

@ -12,12 +12,13 @@ For example, in a traditional `Map`, setting a key would only set it on the loca
:::tip[Differences between Map and SharedMap]
- SharedMaps *must* use string keys.
- SharedMaps _must_ use string keys.
- You must only store the following as values in a `SharedMap`:
- *Plain objects* -- those that are safely JSON-serializable.
- _Plain objects_ -- those that are safely JSON-serializable.
If you store class instances, for example, then data synchronization will not work as expected.
- [Handles](/docs/concepts/handles) to other Fluid DDSes
- When storing objects as values in a SharedMap, changes to the object will be synchronized whole-for-whole. This means that individual changes to the properties of an object are not merged during synchronization. If you need this behavior you should store individual properties in the SharedMap instead of full objects. See [Picking the right data structure](../build/dds#picking-the-right-data-structure) for more information.
:::
For additional background on DDSes and a general overview of their design, see [Introducing distributed data structures](../build/dds).
@ -50,8 +51,8 @@ The following example loads a `SharedMap` as part of the initial roster of objec
const schema = {
initialObjects: {
customMap: SharedMap,
}
}
},
};
const { container, services } = await client.createContainer(schema);
@ -66,8 +67,8 @@ Similarly, if you are loading an existing container, the process stays largely i
const schema = {
initialObjects: {
customMap: SharedMap,
}
}
},
};
const { container, services } = await client.getContainer(id, schema);
@ -133,13 +134,13 @@ Consider the following example where you have a label and a button. When clicked
```javascript
const map = container.initialObjects.customMap;
const dataKey = "data";
const button = document.createElement('button');
const button = document.createElement("button");
button.textContent = "Randomize!";
const label = document.createElement('label');
const label = document.createElement("label");
button.addEventListener('click', () =>
button.addEventListener("click", () =>
// Set the new value on the SharedMap
map.set(dataKey, Math.random())
map.set(dataKey, Math.random()),
);
// This function will update the label from the SharedMap.
@ -149,7 +150,7 @@ const updateLabel = () => {
const value = map.get(dataKey) || 0;
label.textContent = `${value}`;
};
map.on('valueChanged', updateLabel);
map.on("valueChanged", updateLabel);
// Make sure updateLabel is called at least once.
updateLabel();
@ -162,13 +163,11 @@ Your event listener can be more sophisticated by using the additional informatio
```javascript {linenos=inline,hl_lines=["14-15"]}
const map = container.initialObjects.customMap;
const dataKey = "data";
const button = document.createElement('button');
const button = document.createElement("button");
button.textContent = "Randomize!";
const label = document.createElement('label');
const label = document.createElement("label");
button.addEventListener('click', () =>
map.set(dataKey, Math.random())
);
button.addEventListener("click", () => map.set(dataKey, Math.random()));
// Get the current value of the shared data to update the view whenever it changes.
const updateLabel = (changed, local) => {
@ -178,7 +177,7 @@ const updateLabel = (changed, local) => {
};
updateLabel(undefined, false);
// Use the changed event to trigger the rerender whenever the value changes.
map.on('valueChanged', updateLabel);
map.on("valueChanged", updateLabel);
```
Now, with the changes in `updateLabel`, the label will update to say if the value was last updated by the current user or by someone else. It will also compare the current value to the last one, and if the value has increased, it will set the text color to green. Otherwise, it will be red.
@ -230,7 +229,7 @@ And each task may also have multiple fields, including the person that it is ass
Now, the next question to ask is which of these fields you'd like to be individually collaborative. For the sake of this example, assume that the `title` and `description` are user-entered values that you'd like people to be able to edit together whereas the `assignedTo` person data is something that you receive from a backend service call that you'd like to store with your object. You can change which person the task gets assigned to but the actual metadata of each person is based off of the returned value from the backend service.
The most direct -- *but ultimately flawed* -- approach here would be to just to store the entire object into the `SharedMap` under a singular key.
The most direct -- _but ultimately flawed_ -- approach here would be to just to store the entire object into the `SharedMap` under a singular key.
This would look something like this:
@ -308,7 +307,7 @@ const pickedUser = users[pickedUserIndex];
// Now store this user object as a whole into the SharedMap
const task = map.get("task1");
map.set(task.assignedToKey, pickedUser)
map.set(task.assignedToKey, pickedUser);
```
This will work as expected **because the entire object is being stored each time** instead of specific fields.
@ -330,8 +329,8 @@ const schema = {
initialObjects: {
initialMap: SharedMap,
},
dynamicObjectTypes: [SharedMap]
}
dynamicObjectTypes: [SharedMap],
};
```
Now, you can dynamically create additional `SharedMap` instances and store their handles into the initial map that is always provided in the container.
@ -437,8 +436,8 @@ const schema = {
initialObjects: {
initialMap: SharedMap,
},
dynamicObjectTypes: [SharedMap]
}
dynamicObjectTypes: [SharedMap],
};
const { container, services } = await client.getContainer(id, schema);
@ -457,7 +456,7 @@ For example, if you wanted to fetch task with ID `task123` and allow the user to
```javascript
const taskHandle = initialMap.get("task123");
const task = await taskHandle.get();
task.set("description", editedDescription)
task.set("description", editedDescription);
```
Since each task is stored in a separate `SharedMap` and all of the fields within the task object are being stored in their own unique keys, your data now has a hierarchical structure that reflects the app's data model while individual tasks' properties can be edited independently.

Просмотреть файл

@ -22,7 +22,7 @@ Because users can simultaneously change the same DDS, you need to consider which
:::note[Meaning of "simultaneously"]
Two or more clients are said to make a change *simultaneously* if they each make a change before they have received the
Two or more clients are said to make a change _simultaneously_ if they each make a change before they have received the
others' changes from the server.
:::
@ -32,8 +32,8 @@ Choosing the correct data structure for your scenario can improve the performanc
DDSes vary from each other by three characteristics:
- **Basic data structure:** For example, key-value pair, a sequence, or a queue.
- **Client autonomy vs. Consensus:** An *optimistic* DDS enables any client to unilaterally change a value and the new
value is relayed to all other clients, while a *consensus-based* DDS will only allow a change if it is accepted by other clients via a
- **Client autonomy vs. Consensus:** An _optimistic_ DDS enables any client to unilaterally change a value and the new
value is relayed to all other clients, while a _consensus-based_ DDS will only allow a change if it is accepted by other clients via a
consensus process.
- **Merge policy:** The policy that determines how conflicting changes from clients are resolved.

Просмотреть файл

@ -32,7 +32,7 @@ Marker segments are never split or merged, and always have a length of 1.
ReferenceType.Simple,
// Arbitrary bag of properties to associate to the marker. If the marker is annotated by a future operation,
// those annotated properties will be merged with the initial set.
{ type: "pg" }
{ type: "pg" },
);
sharedString.insertText(3, "world");
// content: hi<pg marker>world
@ -49,7 +49,6 @@ Marker segments are never split or merged, and always have a length of 1.
// Handle text segments (e.g. append to the current text accumulator with `segment.text`, apply any formatting on `segment.props`)
}
});
```
<!-- It might be worth adding a section about Tile markers and their use: setting ReferenceType.Tile and putting a label on [reservedTileLabelsKey] allows

Просмотреть файл

@ -3,7 +3,7 @@ title: Connect to Azure Fluid Relay
sidebar_position: 2
---
import { PackageLink } from "@site/src/components/shortLinks"
import { PackageLink } from "@site/src/components/shortLinks";
[Azure Fluid Relay](https://aka.ms/azurefluidrelay) is a cloud-hosted Fluid service.
You can connect your Fluid application to an Azure Fluid Relay instance using the `AzureClient` in the <PackageLink packageName="azure-client">@fluidframework/azure-client</PackageLink> package.

Просмотреть файл

@ -3,7 +3,7 @@ title: Available Fluid services
sidebar_position: 1
---
import { PackageLink } from "@site/src/components/shortLinks"
import { PackageLink } from "@site/src/components/shortLinks";
The Fluid Framework can be used with any compatible service implementation. Some services, like Tinylicious, are intended only for testing and development, while other hosted options provide the high scalability needed for production-quality applications.

Просмотреть файл

@ -3,7 +3,7 @@ title: Frequently Asked Questions
sidebar_position: 7
---
import Browsers from '@site/src/pages/browsers.mdx';
import Browsers from "@site/src/pages/browsers.mdx";
The following are short, sometimes superficial, answers to some of the most commonly asked questions about the Fluid
Framework.
@ -21,7 +21,7 @@ The Fluid Framework was designed with performance and ease of development as top
### What is a DDS?
DDS is short for *distributed data structure*. DDSes are the foundation of the Fluid Framework. They are designed such
DDS is short for _distributed data structure_. DDSes are the foundation of the Fluid Framework. They are designed such
that the Fluid runtime is able to keep them in sync across clients while each client operates on the DDSes in largely
the same way they would operate on local data. The data source for a Fluid solution can represent numerous DDSes.
@ -44,7 +44,7 @@ sessions later and for efficiencies when saving to persistent storage.
**Persistent storage** is a record of ops (and summary ops) saved outside of the Fluid service. This could be a
database, blob storage, or a file. Using persistent storage allows a Fluid solution to persist across sessions.
For example, current Microsoft 365 Fluid experiences save ops in *.fluid* files in SharePoint and OneDrive.
For example, current Microsoft 365 Fluid experiences save ops in _.fluid_ files in SharePoint and OneDrive.
It is important to note that these files share many of the properties of a normal file such as permissions and a
location in a file structure, but because these experiences rely on the Fluid service, downloading the files and
working locally is not supported.

Просмотреть файл

@ -5,7 +5,7 @@ sidebar_position: 8
## Attached
A Fluid container can be *attached* or *detached*. An attached container is connected to a Fluid service and can be
A Fluid container can be _attached_ or _detached_. An attached container is connected to a Fluid service and can be
loaded by other clients. Also see [Detached](#detached).
## Code loader
@ -30,7 +30,7 @@ as well as providing an API surface to your data.
## Detached
A Fluid container can be *attached* or *detached*. A detached container is not connected to a Fluid service and cannot
A Fluid container can be _attached_ or _detached_. A detached container is not connected to a Fluid service and cannot
be loaded by other clients. Newly created containers begin in a detached state, which allows developers to add initial
data if needed before attaching the container. Also see [Attached](#attached).
@ -61,5 +61,5 @@ A distributed data structure (DDS) or Data Object.
## URL resolver
Fluid's API surface makes use of URLs, for example in the `Loader`'s `resolve()` method and `Container`'s `request()`
method. The URL resolver is used to interpret these URLs for use with the Fluid service.
Fluid's API surface makes use of URLs, for example in the `Loader`'s `resolve()` method and `Container`'s `request()` method.
The URL resolver is used to interpret these URLs for use with the Fluid service.

Просмотреть файл

@ -16,9 +16,9 @@ Because building low-latency, collaborative experiences is hard!
Fluid Framework offers:
* Client-centric application model with data persistence requiring no custom server code.
* Distributed data structures with familiar programming patterns.
* Very low latency.
- Client-centric application model with data persistence requiring no custom server code.
- Distributed data structures with familiar programming patterns.
- Very low latency.
The developers at Microsoft have built collaboration into many applications, but many required application specific
server-side logic to manage the collaborative experience. The Fluid Framework is the result of Microsoft's investment

Просмотреть файл

@ -5,7 +5,7 @@ sidebar_position: 9
sidebar_label: Release Notes
---
import { ApiLink } from "@site/src/components/shortLinks"
import { ApiLink } from "@site/src/components/shortLinks";
# 1.3.0
@ -27,7 +27,6 @@ We are now surfacing errors on FluidContainer "disposed" event, so the applicati
## @fluidframework/azure-client
- Fix <ApiLink packageName="azure-client" apiName="AzureClient" apiType="class" /> issue where the second user could not connect in `local` mode
# 1.0.0
@ -38,8 +37,22 @@ We are now surfacing errors on FluidContainer "disposed" event, so the applicati
We've added two new methods to AzureClient that will enable developers to recover data from corrupted containers. The Fluid Framework automatically generates and saves snapshots of the operation stream that we use to load the latest container. `getContainerVersions` will allow developers to access previous versions of the container. `copyContainer` allows developers to generate a new detached container from another container.
- <ApiLink packageName="azure-client" apiName="AzureClient" apiType="class" headingId="getcontainerversions-method">`getContainerVersions(id, options)`</ApiLink>
- <ApiLink packageName="azure-client" apiName="AzureClient" apiType="class" headingId="copycontainer-method">`copyContainer(id, containerSchema)`</ApiLink>
- <ApiLink
packageName="azure-client"
apiName="AzureClient"
apiType="class"
headingId="getcontainerversions-method"
>
`getContainerVersions(id, options)`
</ApiLink>
- <ApiLink
packageName="azure-client"
apiName="AzureClient"
apiType="class"
headingId="copycontainer-method"
>
`copyContainer(id, containerSchema)`
</ApiLink>
In an situation where a container will fail to load these two methods can be used together to load a document from a previous state in time.

Просмотреть файл

@ -55,7 +55,6 @@ action copy the full URL in the browser, including the ID, into a new window or
second client for your dice roller application. With both windows open, click the **Roll** button in either and note
that the state of the dice changes in both clients.
🥳**Congratulations**🎉 You have successfully taken the first step towards unlocking the world of Fluid collaboration.
## Next step

Просмотреть файл

@ -1,5 +1,5 @@
---
title: 'Tutorial: DiceRoller application'
title: "Tutorial: DiceRoller application"
sidebar_position: 2
---
@ -29,7 +29,7 @@ All of the work in this demo will be done in the [app.js](https://github.com/mic
Start by creating a new instance of the Tinylicious client. Tinylicious is the Fluid Framework's local testing server, and a client is responsible for creating and loading containers.
The app creates Fluid containers using a schema that defines a set of *initial objects* that will be available in the container. Learn more about initial objects in [Data modeling](../build/data-modeling).
The app creates Fluid containers using a schema that defines a set of _initial objects_ that will be available in the container. Learn more about initial objects in [Data modeling](../build/data-modeling).
Lastly, `root` defines the HTML element that the Dice will render on.
@ -71,7 +71,7 @@ const createNewDice = async () => {
dice.initialize(new Dice({ value: 1 }));
const id = await container.attach();
renderDiceRoller(dice.root, root);
}
};
```
### Loading an existing container
@ -83,7 +83,7 @@ const loadExistingDice = async (id) => {
const { container } = await client.getContainer(id, containerSchema, "2");
const dice = container.initialObjects.diceTree.viewWith(treeViewConfiguration);
renderDiceRoller(dice.root, root);
}
};
```
### Switching between loading and creating
@ -117,7 +117,6 @@ This example uses standard HTML/DOM methods to render a view.
The `renderDiceRoller` function runs only when the container is created or loaded. It appends the `diceTemplate` to the passed in HTML element, and creates a working dice roller with a random dice value each time the "Roll" button is clicked on a client.
```js
const diceTemplate = document.createElement("template");
@ -131,7 +130,7 @@ diceTemplate.innerHTML = `
<div class="dice"></div>
<button class="roll"> Roll </button>
</div>
`
`;
const renderDiceRoller = (dice, elem) => {
elem.appendChild(template.content.cloneNode(true));
@ -139,7 +138,7 @@ const renderDiceRoller = (dice, elem) => {
const diceElem = elem.querySelector(".dice");
/* REMAINDER OF THE FUNCTION IS DESCRIBED BELOW */
}
};
```
## Connect the view to Fluid data
@ -155,7 +154,7 @@ This pattern is common in Fluid because it enables the view to behave the same w
```js
rollButton.onclick = () => {
dice.value = Math.floor(Math.random() * 6) + 1;
}
};
```
### Relying on Fluid data
@ -165,7 +164,7 @@ The next line creates the function that will rerender the local view with the la
- When the container is created or loaded.
- When the dice value changes on any client.
Note that the current value is retrieved from the `SharedMap` each time `updateDice` is called. It is *not* read from the `textContent` of the local `dice` HTML element.
Note that the current value is retrieved from the `SharedMap` each time `updateDice` is called. It is _not_ read from the `textContent` of the local `dice` HTML element.
```js
const updateDice = () => {
@ -173,7 +172,7 @@ Note that the current value is retrieved from the `SharedMap` each time `updateD
// Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅)
diceElem.textContent = String.fromCodePoint(0x267f + diceValue);
diceElem.style.color = `hsl(${diceValue * 60}, 70%, 30%)`;
}
};
```
### Update on creation or load of container
@ -186,7 +185,7 @@ The next line ensures that the dice is rendered as soon as `renderDiceRoller` is
### Handling remote changes
To keep the data up to date as it changes, an event handler must be set on the `dice` object to call `updateDice` each time that the `afterChanged` event is sent. Use the built-in `Tree` object to subscribe to the event. Note that the `afterChanged` event fires whenever the `dice` object changes on *any* client; that is, when the "Roll" button is clicked on any client.
To keep the data up to date as it changes, an event handler must be set on the `dice` object to call `updateDice` each time that the `afterChanged` event is sent. Use the built-in `Tree` object to subscribe to the event. Note that the `afterChanged` event fires whenever the `dice` object changes on _any_ client; that is, when the "Roll" button is clicked on any client.
```js
Tree.on(dice, "afterChange", updateDice);

Просмотреть файл

@ -3,7 +3,7 @@ title: Logging and telemetry
sidebar_position: 2
---
import { ApiLink } from "@site/src/components/shortLinks"
import { ApiLink } from "@site/src/components/shortLinks";
Telemetry is an essential part of maintaining the health of modern applications. Fluid Framework provides a way to plug
in your own logic to handle telemetry events sent by Fluid. This enables you to integrate the Fluid telemetry along with
@ -32,8 +32,14 @@ The `Loader` constructor is called by both `createContainer()` and `getContainer
interface as its constructor argument. `ILoaderProps` interface has an optional logger parameter that will take the
`ITelemetryBaseLogger` defined by the user.
<ApiLink packageName="container-loader" apiName="ILoaderProps" apiType="interface" headingId="logger-propertysignature">ILoaderProps.logger</ApiLink>
<ApiLink
packageName="container-loader"
apiName="ILoaderProps"
apiType="interface"
headingId="logger-propertysignature"
>
ILoaderProps.logger
</ApiLink>
is used by `Loader` to pipe to container's telemetry system.
### Properties and methods
@ -116,8 +122,8 @@ export interface ITelemetryProperties {
}
export interface ITaggedTelemetryPropertyType {
value: TelemetryEventPropertyType,
tag: string,
value: TelemetryEventPropertyType;
tag: string;
}
export type TelemetryEventPropertyType = string | number | boolean | undefined;
@ -150,7 +156,7 @@ The Fluid Framework sends events in the following categories:
### EventName
This property contains a unique name for the event. The name may be namespaced, delimitted by a colon ':'.
Additionally, some event names (not the namespaces) contain underscores '_', as a free-form subdivision of
Additionally, some event names (not the namespaces) contain underscores '\_', as a free-form subdivision of
events into different related cases. Once common example is `foo_start`, `foo_end` and `foo_cancel` for
performance events.
@ -206,8 +212,10 @@ this.logger.sendTelemetryEvent({
pendingClientId: this.pendingClientId,
clientId: this.clientId,
hasTimer: this.prevClientLeftTimer.hasTimer,
inQuorum: protocolHandler !== undefined && this.pendingClientId !== undefined
&& protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
inQuorum:
protocolHandler !== undefined &&
this.pendingClientId !== undefined &&
protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
});
```
@ -260,8 +268,11 @@ async function start(): Promise<void> {
Now, whenever a telemetry event is encountered, the custom `send()` method gets called and will print out the entire
event object.
<img src="https://storage.fluidframework.com/static/images/consoleLogger_telemetry_in_action.png" alt="The
ConsoleLogger sends telemetry events to the browser console for display."/>
<img
src="https://storage.fluidframework.com/static/images/consoleLogger_telemetry_in_action.png"
alt="The
ConsoleLogger sends telemetry events to the browser console for display."
/>
:::warning
@ -280,7 +291,7 @@ in both Node.js and a web browser.
after which you will need to reload the page.
```js
localStorage.debug = 'fluid:*'
localStorage.debug = "fluid:*";
```
You'll also need to enable the `Verbose` logging level in the console. The dropdown that controls that is just above it,

Просмотреть файл

@ -46,7 +46,7 @@ The Azure Fluid Relay client can also connect to a local Tinylicious instance.
About this code note:
* The values for `tenantId`, `endpoint`, and `type` correspond to those for Tinylicious, where `7070` is the default port for Tinylicious.
- The values for `tenantId`, `endpoint`, and `type` correspond to those for Tinylicious, where `7070` is the default port for Tinylicious.
```javascript
const user = {
@ -61,7 +61,7 @@ const config = {
const clientProps = {
connection: config,
}
};
// This AzureClient instance connects to a local Tinylicious
// instance rather than a live Azure Fluid Relay
@ -79,15 +79,17 @@ The target service variable can be set as part of the test script, while secrets
```typescript
function createAzureClient(): AzureClient {
const useAzure = process.env.FLUID_CLIENT === "azure";
const tenantKey = useAzure ? process.env.FLUID_TENANTKEY as string : "";
const tenantKey = useAzure ? (process.env.FLUID_TENANTKEY as string) : "";
const user = { id: "userId", name: "Test User" };
const connectionConfig = useAzure ? {
const connectionConfig = useAzure
? {
type: "remote",
tenantId: "myTenantId",
tokenProvider: new InsecureTokenProvider(tenantKey, user),
endpoint: "https://myOrdererUrl",
} : {
}
: {
type: "local",
tokenProvider: new InsecureTokenProvider("fooBar", user),
endpoint: "http://localhost:7070",
@ -113,14 +115,13 @@ describe("ClientTest", () => {
it("can create Azure container successfully", async () => {
const schema: ContainerSchema = {
initialObjects: {
customMap: SharedMap
customMap: SharedMap,
},
};
const containerAndServices = await client.createContainer(schema);
});
});
```
You can then use the following npm scripts:

Просмотреть файл

@ -71,7 +71,7 @@ To use Tinylicious with ngrok, use the following steps. If you do not have an ng
ngrok http PORT_NUMBER
```
After completing the final step, you will see the *Forwarding URL* in your terminal, which can be used to access Tinylicious.
After completing the final step, you will see the _Forwarding URL_ in your terminal, which can be used to access Tinylicious.
Note that the ngrok URL supports both HTTP and HTTPS tunneling through to your local server.

Просмотреть файл

@ -1,4 +1,5 @@
import {Redirect} from '@docusaurus/router';
import { Redirect } from "@docusaurus/router";
{/* The local docs view only contains API docs, so just redirect to them if someone navigates to this url manually. */}
<Redirect to="./api" />