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:
Родитель
53dd76c2c5
Коммит
233bcc4c08
|
@ -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,8 +10,9 @@ 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))
|
||||
|
||||
- [@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))
|
||||
|
||||
For more information on our service client implementations, see [Fluid Services](../deployment/service-options).
|
||||
|
|
|
@ -3,17 +3,16 @@ title: User presence and audience
|
|||
sidebar_position: 8
|
||||
---
|
||||
|
||||
The audience is the collection of users connected to a container. When your app creates a container using a service-specific client library, the app is provided with a service-specific audience object for that container as well. Your code can query the audience object for connected users and use that information to build rich and collaborative user presence features.
|
||||
The audience is the collection of users connected to a container. When your app creates a container using a service-specific client library, the app is provided with a service-specific audience object for that container as well. Your code can query the audience object for connected users and use that information to build rich and collaborative user presence features.
|
||||
|
||||
This document will explain how to use the audience APIs and then provide examples on how to use the audience to show user presence. For anything service-specific, the [Tinylicious](/docs/testing/tinylicious) Fluid service is used.
|
||||
This document will explain how to use the audience APIs and then provide examples on how to use the audience to show user presence. For anything service-specific, the [Tinylicious](/docs/testing/tinylicious) Fluid service is used.
|
||||
|
||||
## Working with the audience
|
||||
|
||||
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.
|
||||
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;
|
||||
```
|
||||
|
||||
|
@ -23,12 +22,15 @@ Audience members exist as `IMember` objects:
|
|||
|
||||
```typescript
|
||||
export interface IMember {
|
||||
id: string;
|
||||
connections: IConnection[];
|
||||
id: string;
|
||||
connections: IConnection[];
|
||||
}
|
||||
```
|
||||
|
||||
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,12 +40,11 @@ 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
|
||||
export interface TinyliciousMember extends IMember {
|
||||
userName: string;
|
||||
userName: string;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -104,7 +106,7 @@ For example, on the creating computer, you don't want to call `container.attach`
|
|||
```typescript
|
||||
// Code that runs only on a creating client.
|
||||
if (container.attachState !== AttachState.Attached) {
|
||||
await container.attach();
|
||||
await container.attach();
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -112,10 +114,11 @@ 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)) {
|
||||
await container.attach();
|
||||
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)) {
|
||||
// Disable editing.
|
||||
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,20 +167,20 @@ 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", () => {
|
||||
// All pending edits have been saved.
|
||||
// All pending edits have been 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", () => {
|
||||
// The container has pending changes that need to be acknowledged by the service.
|
||||
// The container has pending changes that need to be acknowledged by the service.
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -272,31 +285,34 @@ Your code can disable the editing UI in your application when the container is d
|
|||
|
||||
```typescript
|
||||
container.on("disconnected", () => {
|
||||
// Prevent user edits to disable data loss.
|
||||
// Prevent user edits to disable data loss.
|
||||
});
|
||||
|
||||
container.on("connected", () => {
|
||||
// Enable editing if disabled.
|
||||
// Enable editing if disabled.
|
||||
});
|
||||
```
|
||||
|
||||
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", () => {
|
||||
// Disconnect the container when the user is idle.
|
||||
container.disconnect();
|
||||
// Disconnect the container when the user is idle.
|
||||
container.disconnect();
|
||||
});
|
||||
|
||||
user.on("active", () => {
|
||||
// Connect the container when the user is active again.
|
||||
container.connect();
|
||||
// Connect the container when the user is active again.
|
||||
container.connect();
|
||||
});
|
||||
```
|
||||
|
||||
### 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,12 +327,12 @@ 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
|
||||
container.on("disposed", () => {
|
||||
// Handle event cleanup to prevent memory leaks.
|
||||
// Handle event cleanup to prevent memory leaks.
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -51,11 +50,11 @@ This example schema defines two initial objects, `layout` and `text`, and declar
|
|||
|
||||
```typescript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
text: SharedString
|
||||
},
|
||||
dynamicObjectTypes: [ SharedCell, SharedString ],
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
text: SharedString,
|
||||
},
|
||||
dynamicObjectTypes: [SharedCell, SharedString],
|
||||
};
|
||||
```
|
||||
|
||||
|
@ -65,13 +64,12 @@ Containers are created by passing the schema to the service-specific client's `c
|
|||
|
||||
```typescript {linenos=inline,hl_lines=[7,8]}
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } =
|
||||
await client.createContainer(schema);
|
||||
const { container, services } = await client.createContainer(schema);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
@ -100,13 +98,12 @@ Note that once published, a container cannot be unpublished. (But it can be dele
|
|||
|
||||
```typescript {linenos=inline,hl_lines=[10]}
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
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.
|
||||
|
|
|
@ -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.
|
||||
|
@ -28,11 +28,11 @@ About this code note:
|
|||
|
||||
```typescript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
"custom-cell": SharedCell,
|
||||
}
|
||||
}
|
||||
initialObjects: {
|
||||
"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*, *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,
|
||||
|
@ -122,7 +122,7 @@ recalculate a derived value when some data in a DDS changes.
|
|||
|
||||
```ts
|
||||
myMap.on("valueChanged", () => {
|
||||
recalculate();
|
||||
recalculate();
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -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.
|
||||
|
@ -144,24 +144,24 @@ Let's assume for a moment that all of the data about a shape is stored as a sing
|
|||
|
||||
```json
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
{
|
||||
"aShape": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
}
|
||||
"aShape": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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 */}
|
||||
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -41,8 +41,8 @@ The following is an example of how to enable experimental features with `AzureCl
|
|||
|
||||
```typescript
|
||||
const featureGates = {
|
||||
"Fluid.ContainerRuntime.ExampleFeature1": true,
|
||||
"Fluid.ContainerRuntime.ExampleFeature2": ["exampleConfig1", "exampleConfig2"],
|
||||
"Fluid.ContainerRuntime.ExampleFeature1": true,
|
||||
"Fluid.ContainerRuntime.ExampleFeature2": ["exampleConfig1", "exampleConfig2"],
|
||||
};
|
||||
```
|
||||
|
||||
|
@ -50,8 +50,8 @@ The following is an example of how to enable experimental features with `AzureCl
|
|||
|
||||
```typescript
|
||||
const azureClient = new AzureClient({
|
||||
connection: connectionProps,
|
||||
logger: myLogger,
|
||||
configProvider: configProvider(featureGates),
|
||||
connection: connectionProps,
|
||||
logger: myLogger,
|
||||
configProvider: configProvider(featureGates),
|
||||
});
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
@ -14,20 +13,17 @@ This section covers how to consume and use Fluid handles.
|
|||
## Why 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.
|
||||
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.
|
||||
|
||||
- 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
|
||||
Fluid runtime to retrieve the object.
|
||||
This reduces the complexity from the caller by abstracting away the need to know how to make a `request` to the
|
||||
Fluid runtime to retrieve the object.
|
||||
|
||||
- Handles enable the underlying Fluid runtime to build a dependency hierarchy. This will enable us to add garbage
|
||||
collection to the runtime in a future version.
|
||||
collection to the runtime in a future version.
|
||||
|
||||
## Basic Scenario
|
||||
|
||||
|
@ -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.
|
||||
|
@ -34,9 +33,9 @@ Here is a look at how you would go about loading `Signaler` as part of the initi
|
|||
|
||||
```typescript
|
||||
const containerSchema: ContainerSchema = {
|
||||
initialObjects: {
|
||||
signaler: Signaler,
|
||||
},
|
||||
initialObjects: {
|
||||
signaler: Signaler,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } = await client.createContainer(containerSchema);
|
||||
|
@ -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";
|
||||
|
@ -68,7 +69,7 @@ private static readonly focusRequestType = "focusRequest";
|
|||
|
||||
```typescript
|
||||
container.on("connected", () => {
|
||||
this.signaler.submitSignal(FocusTracker.focusRequestType);
|
||||
this.signaler.submitSignal(FocusTracker.focusRequestType);
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -76,11 +77,14 @@ The connected clients are listening to this focus request signal, and they respo
|
|||
|
||||
```typescript
|
||||
this.signaler.onSignal(FocusTracker.focusRequestType, () => {
|
||||
this.sendFocusSignal(document.hasFocus());
|
||||
this.sendFocusSignal(document.hasFocus());
|
||||
});
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
@ -88,36 +92,36 @@ Rather than submitting multiple signals in response to an event, it is more cost
|
|||
|
||||
```typescript
|
||||
container.on("connected", () => {
|
||||
this.signaler.submitSignal("colorRequest");
|
||||
this.signaler.submitSignal("focusRequest");
|
||||
this.signaler.submitSignal("currentlySelectedObjectRequest");
|
||||
this.signaler.submitSignal("colorRequest");
|
||||
this.signaler.submitSignal("focusRequest");
|
||||
this.signaler.submitSignal("currentlySelectedObjectRequest");
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
this.signaler.onSignal("colorRequest", (clientId, local, payload) => {
|
||||
/*...*/
|
||||
/*...*/
|
||||
});
|
||||
this.signaler.onSignal("focusRequest", (clientId, local, payload) => {
|
||||
/*...*/
|
||||
/*...*/
|
||||
});
|
||||
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", () => {
|
||||
this.signaler.submitSignal("connectRequest");
|
||||
this.signaler.submitSignal("connectRequest");
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
this.signaler.onSignal("connectRequest", (clientId, local, payload) => {
|
||||
/*...*/
|
||||
/*...*/
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
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)).
|
||||
- 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)).
|
||||
|
||||
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.
|
||||
If you store class instances, for example, then data synchronization will not work as expected.
|
||||
- _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).
|
||||
|
@ -48,10 +52,10 @@ The following example loads a `SharedMap` as part of the initial roster of objec
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
}
|
||||
}
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } = await client.createContainer(schema);
|
||||
|
||||
|
@ -64,10 +68,10 @@ Similarly, if you are loading an existing container, the process stays largely i
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
}
|
||||
}
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } = await client.getContainer(id, schema);
|
||||
|
||||
|
@ -103,7 +107,7 @@ Each edit will also trigger a `valueChanged` event which will be discussed in th
|
|||
- `entries()` -- Returns an iterator for all key/value pairs stored in the map
|
||||
- `delete(key)` -- Removes the key/value pair from the map
|
||||
- `forEach(callbackFn: (value, key, map) => void)` -- Applies the provided function to each entry in the map.
|
||||
For example, the following will print out all of the key/value pairs in the map
|
||||
For example, the following will print out all of the key/value pairs in the map
|
||||
|
||||
```javascript
|
||||
this.map.forEach((value, key) => console.log(`${key}-${value}`));
|
||||
|
@ -133,23 +137,23 @@ 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', () =>
|
||||
// Set the new value on the SharedMap
|
||||
map.set(dataKey, Math.random())
|
||||
button.addEventListener("click", () =>
|
||||
// Set the new value on the SharedMap
|
||||
map.set(dataKey, Math.random()),
|
||||
);
|
||||
|
||||
// This function will update the label from the SharedMap.
|
||||
// It is connected to the SharedMap's valueChanged event,
|
||||
// and will be called each time a value in the SharedMap is changed.
|
||||
const updateLabel = () => {
|
||||
const value = map.get(dataKey) || 0;
|
||||
label.textContent = `${value}`;
|
||||
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,23 +166,21 @@ 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) => {
|
||||
const value = map.get(dataKey) || 0;
|
||||
label.textContent = `${value} from ${local ? "me" : "someone else"}`;
|
||||
label.style.color = changed?.previousValue > value ? "red" : "green";
|
||||
const value = map.get(dataKey) || 0;
|
||||
label.textContent = `${value} from ${local ? "me" : "someone else"}`;
|
||||
label.style.color = changed?.previousValue > value ? "red" : "green";
|
||||
};
|
||||
updateLabel(undefined, false);
|
||||
// Use the changed event to trigger the rerender whenever the value changes.
|
||||
map.on('valueChanged', updateLabel);
|
||||
// Use the changed event to trigger the rerender whenever the value changes.
|
||||
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.
|
||||
|
@ -208,9 +210,9 @@ Here, each person may have multiple fields such as
|
|||
|
||||
```json
|
||||
{
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -218,33 +220,33 @@ And each task may also have multiple fields, including the person that it is ass
|
|||
|
||||
```json
|
||||
{
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -260,18 +262,18 @@ You can store each of these values in their own key and only hold the key at whi
|
|||
|
||||
```json
|
||||
{
|
||||
"task1": {
|
||||
"titleKey": "task1Title",
|
||||
"descriptionKey": "task1Description",
|
||||
"assignedToKey": "task1AssignedTo"
|
||||
},
|
||||
"task1Title": "Awesome Task",
|
||||
"task1Description": "Doing the most awesome things",
|
||||
"task1AssignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
"task1": {
|
||||
"titleKey": "task1Title",
|
||||
"descriptionKey": "task1Description",
|
||||
"assignedToKey": "task1AssignedTo"
|
||||
},
|
||||
"task1Title": "Awesome Task",
|
||||
"task1Description": "Doing the most awesome things",
|
||||
"task1AssignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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.
|
||||
|
@ -327,11 +329,11 @@ The following example demonstrates nesting DDSes using `SharedMap`. You specify
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
initialMap: SharedMap,
|
||||
},
|
||||
dynamicObjectTypes: [SharedMap]
|
||||
}
|
||||
initialObjects: {
|
||||
initialMap: 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.
|
||||
|
@ -370,24 +372,24 @@ You can further extend the example from the [Storing objects](#storing-objects)
|
|||
|
||||
```json
|
||||
{
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
},
|
||||
"task2": {
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
}
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
},
|
||||
"task2": {
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -404,13 +406,13 @@ And the `task1` map would look like:
|
|||
|
||||
```json
|
||||
{
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -418,13 +420,13 @@ And the `task2` map would look like:
|
|||
|
||||
```json
|
||||
{
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -434,11 +436,11 @@ Whenever a new task is created, you can call `container.create` to create a new
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
initialMap: SharedMap,
|
||||
},
|
||||
dynamicObjectTypes: [SharedMap]
|
||||
}
|
||||
initialObjects: {
|
||||
initialMap: 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,9 +32,9 @@ 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
|
||||
consensus process.
|
||||
- **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.
|
||||
|
||||
Below we've enumerated the data structures and described when they may be most useful.
|
||||
|
@ -55,10 +55,10 @@ Although the value of a pair can be a complex object, the value of any given pai
|
|||
### Common Problems
|
||||
|
||||
- Storing a lot of data in one key-value entry may cause performance or merge issues.
|
||||
Each update will update the entire value rather than merging two updates.
|
||||
Try splitting the data across multiple keys.
|
||||
Each update will update the entire value rather than merging two updates.
|
||||
Try splitting the data across multiple keys.
|
||||
- Storing arrays, lists, or logs in a single key-value entry may lead to unexpected behavior because users can't collaboratively modify parts of one entry.
|
||||
Instread, try storing the data in an array node in a [SharedTree](#sharedtree).
|
||||
Instread, try storing the data in an array node in a [SharedTree](#sharedtree).
|
||||
|
||||
## SharedString
|
||||
|
||||
|
|
|
@ -24,32 +24,31 @@ matching the length of the text content they contain.
|
|||
Marker segments are never split or merged, and always have a length of 1.
|
||||
|
||||
```typescript
|
||||
// content: hi
|
||||
// positions: 01
|
||||
// content: hi
|
||||
// positions: 01
|
||||
|
||||
sharedString.insertMarker(
|
||||
2,
|
||||
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" }
|
||||
);
|
||||
sharedString.insertText(3, "world");
|
||||
// content: hi<pg marker>world
|
||||
// positions: 01 2 34567
|
||||
sharedString.insertMarker(
|
||||
2,
|
||||
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" },
|
||||
);
|
||||
sharedString.insertText(3, "world");
|
||||
// content: hi<pg marker>world
|
||||
// positions: 01 2 34567
|
||||
|
||||
// Since markers don't directly correspond to text, they aren't included in direct text queries
|
||||
sharedString.getText(); // returns "hiworld"
|
||||
|
||||
// Instead, rich text models involving markers likely want to read from the SharedString using `walkSegments`:
|
||||
sharedString.walkSegments((segment) => {
|
||||
if (Marker.is(segment)) {
|
||||
// Handle markers (e.g. dereference and insert image data, interpret marker properties, etc.)
|
||||
} else {
|
||||
// Handle text segments (e.g. append to the current text accumulator with `segment.text`, apply any formatting on `segment.props`)
|
||||
}
|
||||
});
|
||||
// Since markers don't directly correspond to text, they aren't included in direct text queries
|
||||
sharedString.getText(); // returns "hiworld"
|
||||
|
||||
// Instead, rich text models involving markers likely want to read from the SharedString using `walkSegments`:
|
||||
sharedString.walkSegments((segment) => {
|
||||
if (Marker.is(segment)) {
|
||||
// Handle markers (e.g. dereference and insert image data, interpret marker properties, etc.)
|
||||
} else {
|
||||
// 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,31 +102,35 @@ 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', {
|
||||
id: sf.string,
|
||||
text: sf.string,
|
||||
author: sf.string,
|
||||
lastChanged: sf.number,
|
||||
votes: sf.array(sf.string),
|
||||
const noteSchema = sf.object("Note", {
|
||||
id: sf.string,
|
||||
text: sf.string,
|
||||
author: sf.string,
|
||||
lastChanged: sf.number,
|
||||
votes: sf.array(sf.string),
|
||||
});
|
||||
```
|
||||
|
||||
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', {
|
||||
id: sf.string,
|
||||
text: sf.string,
|
||||
author: sf.string,
|
||||
lastChanged: sf.number,
|
||||
votes: sf.array(sf.string),
|
||||
}) { /* members of the class defined here */ };
|
||||
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 */
|
||||
}
|
||||
```
|
||||
|
||||
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,10 +175,10 @@ 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) {
|
||||
public newNote(author: string) {
|
||||
// implementation omitted.
|
||||
}
|
||||
class Notes extends sf.array("Notes", Note) {
|
||||
public newNote(author: string) {
|
||||
// implementation omitted.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -193,22 +197,22 @@ 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]) {
|
||||
public newNote(author: string) {
|
||||
// implementation omitted.
|
||||
}
|
||||
class Items extends sf.array("Items", [Group, Note]) {
|
||||
public newNote(author: string) {
|
||||
// implementation omitted.
|
||||
}
|
||||
|
||||
public newGroup(name: string): Group {
|
||||
// implementation omitted.
|
||||
}
|
||||
public newGroup(name: string): Group {
|
||||
// implementation omitted.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The root of the schema must itself have a type which is defined as follows:
|
||||
|
||||
```typescript
|
||||
class App extends sf.object('App', {
|
||||
items: Items,
|
||||
class App extends sf.object("App", {
|
||||
items: Items,
|
||||
}) {}
|
||||
```
|
||||
|
||||
|
@ -216,8 +220,8 @@ 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
|
||||
// root node schema
|
||||
schema: App,
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -263,9 +267,9 @@ To create a `TreeView` object, create a container with an initial object of type
|
|||
|
||||
```typescript
|
||||
const containerSchema: ContainerSchema = {
|
||||
initialObjects: {
|
||||
appData: SharedTree,
|
||||
},
|
||||
initialObjects: {
|
||||
appData: SharedTree,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } = await client.createContainer(containerSchema, "2");
|
||||
|
@ -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".
|
||||
|
@ -579,7 +582,7 @@ Returns `true` if `someNode` is of type `nodeType`. Note that `T` is a type that
|
|||
|
||||
```typescript
|
||||
if (Tree.is(myNode, Note)) {
|
||||
// Code here that processes Note nodes.
|
||||
// Code here that processes Note nodes.
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -607,13 +610,12 @@ You can also pass a `TreeView` object to `runTransaction()`.
|
|||
|
||||
```typescript
|
||||
Tree.runTransaction(myTreeView, (treeView) => {
|
||||
// Make multiple changes to the tree.
|
||||
})
|
||||
// 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
|
||||
|
@ -632,8 +634,8 @@ Returns the parent node of `node`. The following snippet continues the sticky no
|
|||
const parent = Tree.parent(note);
|
||||
|
||||
if (Tree.is(parent, Notes) || Tree.is(parent, Items)) {
|
||||
const index = parent.indexOf(note);
|
||||
parent.removeAt(index);
|
||||
const index = parent.indexOf(note);
|
||||
parent.removeAt(index);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
@ -62,4 +62,4 @@ 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.
|
||||
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
|
||||
|
|
|
@ -9,22 +9,24 @@ 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={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-evenly",
|
||||
flexWrap: "wrap",
|
||||
gap: "10px",
|
||||
}} />
|
||||
<MockDiceRollerSample
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-evenly",
|
||||
flexWrap: "wrap",
|
||||
gap: "10px",
|
||||
}}
|
||||
/>
|
||||
|
||||
## Set up your development environment
|
||||
|
||||
To get started you need the following installed.
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
- <NodeVersions />
|
||||
- <NodeVersions />
|
||||
- Code editor
|
||||
- We recommend [Visual Studio Code](https://code.visualstudio.com/).
|
||||
- We recommend [Visual Studio Code](https://code.visualstudio.com/).
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
## Getting started
|
||||
|
@ -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({
|
||||
title: "todo list",
|
||||
items: [
|
||||
new TodoItem({
|
||||
description: "first item",
|
||||
isComplete: true,
|
||||
}),
|
||||
]
|
||||
}));
|
||||
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({
|
||||
description: "new item",
|
||||
isComplete: false,
|
||||
}),);
|
||||
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,27 +214,30 @@ 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) => {
|
||||
if (getRevertible === undefined) {
|
||||
return;
|
||||
}
|
||||
const revertible = getRevertible();
|
||||
if (commit.kind === CommitKind.Undo) {
|
||||
redoStack.push(revertible);
|
||||
} else {
|
||||
if (commit.kind === CommitKind.Default) {
|
||||
// clear redo stack
|
||||
for (const redo of redoStack) {
|
||||
redo.dispose();
|
||||
}
|
||||
redoStack.length = 0;
|
||||
const unsubscribe = appData.events.on(
|
||||
"commitApplied",
|
||||
(commit: CommitMetadata, getRevertible?: RevertibleFactory) => {
|
||||
if (getRevertible === undefined) {
|
||||
return;
|
||||
}
|
||||
undoStack.push(revertible);
|
||||
}
|
||||
});
|
||||
const revertible = getRevertible();
|
||||
if (commit.kind === CommitKind.Undo) {
|
||||
redoStack.push(revertible);
|
||||
} else {
|
||||
if (commit.kind === CommitKind.Default) {
|
||||
// clear redo stack
|
||||
for (const redo of redoStack) {
|
||||
redo.dispose();
|
||||
}
|
||||
redoStack.length = 0;
|
||||
}
|
||||
undoStack.push(revertible);
|
||||
}
|
||||
},
|
||||
);
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: 'Tutorial: DiceRoller application'
|
||||
title: "Tutorial: DiceRoller application"
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
|
@ -7,14 +7,16 @@ 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={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-evenly",
|
||||
flexWrap: "wrap",
|
||||
gap: "10px",
|
||||
}} />
|
||||
<br/>
|
||||
<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,15 +132,15 @@ diceTemplate.innerHTML = `
|
|||
<div class="dice"></div>
|
||||
<button class="roll"> Roll </button>
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
const renderDiceRoller = (dice, elem) => {
|
||||
elem.appendChild(template.content.cloneNode(true));
|
||||
|
||||
const rollButton = elem.querySelector(".roll");
|
||||
const diceElem = elem.querySelector(".dice");
|
||||
|
||||
/* REMAINDER OF THE FUNCTION IS DESCRIBED BELOW */
|
||||
}
|
||||
/* REMAINDER OF THE FUNCTION IS DESCRIBED BELOW */
|
||||
};
|
||||
```
|
||||
|
||||
## Connect the view to Fluid data
|
||||
|
@ -148,14 +149,14 @@ Let's go through the rest of the `renderDiceRoller` function line-by-line.
|
|||
|
||||
### Create the Roll button handler
|
||||
|
||||
The next line of the `renderDiceRoller` function assigns a handler to the click event of the "Roll" button. Instead of updating the local state directly, the button updates the number stored in the `value` property of the `dice` object. Because `dice` is the root object of Fluid `SharedTree`, changes will be distributed to all clients. Any changes to `dice` will cause a `afterChanged` event to be emitted, and an event handler, defined below, can trigger an update of the view.
|
||||
The next line of the `renderDiceRoller` function assigns a handler to the click event of the "Roll" button. Instead of updating the local state directly, the button updates the number stored in the `value` property of the `dice` object. Because `dice` is the root object of Fluid `SharedTree`, changes will be distributed to all clients. Any changes to `dice` will cause a `afterChanged` event to be emitted, and an event handler, defined below, can trigger an update of the view.
|
||||
|
||||
This pattern is common in Fluid because it enables the view to behave the same way for both local and remote changes.
|
||||
|
||||
```js
|
||||
rollButton.onclick = () => {
|
||||
dice.value = Math.floor(Math.random() * 6) + 1;
|
||||
}
|
||||
rollButton.onclick = () => {
|
||||
dice.value = Math.floor(Math.random() * 6) + 1;
|
||||
};
|
||||
```
|
||||
|
||||
### Relying on Fluid data
|
||||
|
@ -165,15 +166,15 @@ 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 = () => {
|
||||
const diceValue = dice.value;
|
||||
// Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅)
|
||||
diceElem.textContent = String.fromCodePoint(0x267f + diceValue);
|
||||
diceElem.style.color = `hsl(${diceValue * 60}, 70%, 30%)`;
|
||||
}
|
||||
const updateDice = () => {
|
||||
const diceValue = dice.value;
|
||||
// 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
|
||||
|
@ -181,15 +182,15 @@ Note that the current value is retrieved from the `SharedMap` each time `updateD
|
|||
The next line ensures that the dice is rendered as soon as `renderDiceRoller` is called, which is when the container is created or loaded.
|
||||
|
||||
```js
|
||||
updateDice();
|
||||
updateDice();
|
||||
```
|
||||
|
||||
### 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);
|
||||
Tree.on(dice, "afterChange", updateDice);
|
||||
```
|
||||
|
||||
## Run the app
|
||||
|
|
|
@ -13,7 +13,7 @@ As Fluid Framework abstracts away the complexities of dealing with real-time col
|
|||
To use the Fluid DevTools, developers need to follow two simple steps:
|
||||
|
||||
1. Integrate the Fluid DevTools library into your app using these steps: https://aka.ms/fluid/devtool
|
||||
- See DevTools integrated into samples at https://aka.ms/fluid/samples
|
||||
- See DevTools integrated into samples at https://aka.ms/fluid/samples
|
||||
2. Install the Fluid DevTools browser extension from [Edge](https://aka.ms/fluid/devtool/edge) or [Chrome](https://aka.ms/fluid/devtool/chrome) extension stores
|
||||
|
||||
Once you have completed the above steps, you can simply launch your Fluid application in a browser window and start the browser DevTools by right-clicking and selecting `Inspect`. In the browser DevTools window, you should see a tab for `Fluid Developer Tools`
|
||||
|
|
|
@ -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
|
||||
|
@ -21,10 +21,10 @@ into the service client props. Both `createContainer()` and `getContainer()` met
|
|||
|
||||
```ts
|
||||
const loader = new Loader({
|
||||
urlResolver: this.urlResolver,
|
||||
documentServiceFactory: this.documentServiceFactory,
|
||||
codeLoader,
|
||||
logger: tinyliciousContainerConfig.logger,
|
||||
urlResolver: this.urlResolver,
|
||||
documentServiceFactory: this.documentServiceFactory,
|
||||
codeLoader,
|
||||
logger: tinyliciousContainerConfig.logger,
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
@ -42,15 +48,15 @@ The interface contains a `send()` method as shown:
|
|||
|
||||
```ts
|
||||
export interface ITelemetryBaseLogger {
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
}
|
||||
```
|
||||
|
||||
- `send()`
|
||||
- The `send()` method is called by the container's telemetry system whenever a telemetry event occurs. This method
|
||||
takes in an ITelemetryBaseEvent type parameter, which is also within the `@fluidframework/common-definitions`
|
||||
package. Given this method is part of an interface, users can implement a custom telemetry logic for the container's
|
||||
telemetry system to execute.
|
||||
takes in an ITelemetryBaseEvent type parameter, which is also within the `@fluidframework/common-definitions`
|
||||
package. Given this method is part of an interface, users can implement a custom telemetry logic for the container's
|
||||
telemetry system to execute.
|
||||
|
||||
### Customizing the logger object
|
||||
|
||||
|
@ -67,10 +73,10 @@ snippets below, or in the `@fluidframework/common-definitions` package for full
|
|||
```ts
|
||||
// @public
|
||||
export interface ITelemetryLogger extends ITelemetryBaseLogger {
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
sendErrorEvent(event: ITelemetryErrorEvent, error?: any): void;
|
||||
sendPerformanceEvent(event: ITelemetryPerformanceEvent, error?: any): void;
|
||||
sendTelemetryEvent(event: ITelemetryGenericEvent, error?: any): void;
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
sendErrorEvent(event: ITelemetryErrorEvent, error?: any): void;
|
||||
sendPerformanceEvent(event: ITelemetryPerformanceEvent, error?: any): void;
|
||||
sendTelemetryEvent(event: ITelemetryGenericEvent, error?: any): void;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -107,17 +113,17 @@ required properties, `eventName` and `category`, are set by the telemetry system
|
|||
|
||||
```ts
|
||||
export interface ITelemetryBaseEvent extends ITelemetryProperties {
|
||||
category: string;
|
||||
eventName: string;
|
||||
category: string;
|
||||
eventName: string;
|
||||
}
|
||||
|
||||
export interface ITelemetryProperties {
|
||||
[index: string]: TelemetryEventPropertyType | ITaggedTelemetryPropertyType;
|
||||
[index: string]: TelemetryEventPropertyType | ITaggedTelemetryPropertyType;
|
||||
}
|
||||
|
||||
export interface ITaggedTelemetryPropertyType {
|
||||
value: TelemetryEventPropertyType,
|
||||
tag: string,
|
||||
value: TelemetryEventPropertyType;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export type TelemetryEventPropertyType = string | number | boolean | undefined;
|
||||
|
@ -132,7 +138,7 @@ either tagged (`ITaggedTelemetryPropertyType`) or untagged (`TelemetryEventPrope
|
|||
|
||||
Tags are strings used to classify the properties on telemetry events. By default, telemetry properties are untagged. However,
|
||||
the Fluid Framework may emit events with some properties tagged, so implementations of `ITelemetryBaseLogger` must be
|
||||
prepared to check for and interpret any tags. Generally speaking, when logging to the user's console, tags can
|
||||
prepared to check for and interpret any tags. Generally speaking, when logging to the user's console, tags can
|
||||
be ignored and tagged values logged plainly, but when transmitting tagged properties to a telemetry service,
|
||||
care should be taken to only log tagged properties where the tag is explicitly understood to indicate the value
|
||||
is safe to log from a data privacy standpoint.
|
||||
|
@ -143,15 +149,15 @@ The Fluid Framework sends events in the following categories:
|
|||
|
||||
- error -- used to identify and report error conditions, e.g. duplicate data store IDs.
|
||||
- performance -- used to track performance-critical code paths within the framework. For example, the summarizer tracks
|
||||
how long it takes to create or load a summary and reports this information in an event.
|
||||
how long it takes to create or load a summary and reports this information in an event.
|
||||
- generic -- used as a catchall for events that are informational and don't represent an activity with a duration like a
|
||||
performance event.
|
||||
performance event.
|
||||
|
||||
### 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
|
||||
events into different related cases. Once common example is `foo_start`, `foo_end` and `foo_cancel` for
|
||||
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.
|
||||
|
||||
### Customizing logged events
|
||||
|
@ -162,12 +168,12 @@ examples:
|
|||
|
||||
```ts
|
||||
if (chunk.version !== undefined) {
|
||||
logger.send({
|
||||
eventName: "MergeTreeChunk:serializeAsMinSupportedVersion",
|
||||
category: "generic",
|
||||
fromChunkVersion: chunk.version,
|
||||
toChunkVersion: undefined,
|
||||
});
|
||||
logger.send({
|
||||
eventName: "MergeTreeChunk:serializeAsMinSupportedVersion",
|
||||
category: "generic",
|
||||
fromChunkVersion: chunk.version,
|
||||
toChunkVersion: undefined,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -201,13 +207,15 @@ Here is the above telemetry event object in action:
|
|||
|
||||
```ts
|
||||
this.logger.sendTelemetryEvent({
|
||||
eventName: "connectedStateRejected",
|
||||
source,
|
||||
pendingClientId: this.pendingClientId,
|
||||
clientId: this.clientId,
|
||||
hasTimer: this.prevClientLeftTimer.hasTimer,
|
||||
inQuorum: protocolHandler !== undefined && this.pendingClientId !== undefined
|
||||
&& protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
|
||||
eventName: "connectedStateRejected",
|
||||
source,
|
||||
pendingClientId: this.pendingClientId,
|
||||
clientId: this.clientId,
|
||||
hasTimer: this.prevClientLeftTimer.hasTimer,
|
||||
inQuorum:
|
||||
protocolHandler !== undefined &&
|
||||
this.pendingClientId !== undefined &&
|
||||
protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -227,10 +235,10 @@ import { ITelemetryBaseLogger, ITelemetryBaseEvent } from "@fluidframework/core-
|
|||
// Define a custom ITelemetry Logger. This logger will be passed into TinyliciousClient
|
||||
// and gets hooked up to the Tinylicious container telemetry system.
|
||||
export class ConsoleLogger implements ITelemetryBaseLogger {
|
||||
constructor() {}
|
||||
send(event: ITelemetryBaseEvent) {
|
||||
console.log("Custom telemetry object array: ".concat(JSON.stringify(event)));
|
||||
}
|
||||
constructor() {}
|
||||
send(event: ITelemetryBaseEvent) {
|
||||
console.log("Custom telemetry object array: ".concat(JSON.stringify(event)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -8,13 +8,13 @@ slug: /testing/testing
|
|||
|
||||
## Overview
|
||||
|
||||
Testing and automation are crucial to maintaining the quality and longevity of your code. Internally, Fluid has a range of unit and integration tests powered by [Mocha](https://mochajs.org/), [Jest](https://jestjs.io/), [Puppeteer](https://github.com/puppeteer/puppeteer), and [webpack](https://webpack.js.org/). Tests that need to run against a service are backed by [Tinylicious](./tinylicious) or a test tenant of a [live service](../deployment/service-options) such as [Azure Fluid Relay](../deployment/azure-frs).
|
||||
Testing and automation are crucial to maintaining the quality and longevity of your code. Internally, Fluid has a range of unit and integration tests powered by [Mocha](https://mochajs.org/), [Jest](https://jestjs.io/), [Puppeteer](https://github.com/puppeteer/puppeteer), and [webpack](https://webpack.js.org/). Tests that need to run against a service are backed by [Tinylicious](./tinylicious) or a test tenant of a [live service](../deployment/service-options) such as [Azure Fluid Relay](../deployment/azure-frs).
|
||||
|
||||
This document will explain how to use these tools to get started with writing automation for Fluid applications against a service. It will focus on interactions with the service rather than automation in general, and will not cover the automation tools themselves or scenarios that do not require a service.
|
||||
This document will explain how to use these tools to get started with writing automation for Fluid applications against a service. It will focus on interactions with the service rather than automation in general, and will not cover the automation tools themselves or scenarios that do not require a service.
|
||||
|
||||
## Automation against Tinylicious
|
||||
|
||||
Automation against Tinylicious is useful for scenarios such as merge validation which want to be unaffected by service interruptions. Your automation should be responsible for starting a local instance of Tinylicious along with terminating it once tests have completed. This example uses the [start-server-and-test package](https://github.com/bahmutov/start-server-and-test) to do this. You can substitute other libraries or implementations.
|
||||
Automation against Tinylicious is useful for scenarios such as merge validation which want to be unaffected by service interruptions. Your automation should be responsible for starting a local instance of Tinylicious along with terminating it once tests have completed. This example uses the [start-server-and-test package](https://github.com/bahmutov/start-server-and-test) to do this. You can substitute other libraries or implementations.
|
||||
|
||||
First install the packages or add them to your dependencies then install:
|
||||
|
||||
|
@ -34,7 +34,7 @@ Once installed, you can use the following npm scripts:
|
|||
}
|
||||
```
|
||||
|
||||
The `test:tinylicious` script will start Tinylicious, wait until port 7070 responds (the default port on which Tinylicious runs), run the test script, and then terminate Tinylicious. Your tests can then use `TinyliciousClient` as usual (see [Tinylicious](./tinylicious)).
|
||||
The `test:tinylicious` script will start Tinylicious, wait until port 7070 responds (the default port on which Tinylicious runs), run the test script, and then terminate Tinylicious. Your tests can then use `TinyliciousClient` as usual (see [Tinylicious](./tinylicious)).
|
||||
|
||||
## Automation against Azure Fluid Relay
|
||||
|
||||
|
@ -42,26 +42,26 @@ Your automation can connect to a test tenant for Azure Fluid Relay in the same w
|
|||
|
||||
### Azure Fluid Relay as an abstraction for Tinylicious
|
||||
|
||||
The Azure Fluid Relay client can also connect to a local Tinylicious instance. This allows you to use a single client type between tests against live and local service instances, where the only difference is the configuration used to create the client.
|
||||
The Azure Fluid Relay client can also connect to a local Tinylicious instance. This allows you to use a single client type between tests against live and local service instances, where the only difference is the configuration used to create the client.
|
||||
|
||||
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 = {
|
||||
id: "UserId",
|
||||
name: "Test User",
|
||||
id: "UserId",
|
||||
name: "Test User",
|
||||
};
|
||||
const config = {
|
||||
type: "local",
|
||||
tokenProvider: new InsecureTokenProvider("fooBar", user),
|
||||
endpoint: "http://localhost:7070",
|
||||
type: "local",
|
||||
tokenProvider: new InsecureTokenProvider("fooBar", user),
|
||||
endpoint: "http://localhost:7070",
|
||||
};
|
||||
|
||||
const clientProps = {
|
||||
connection: config,
|
||||
}
|
||||
connection: config,
|
||||
};
|
||||
|
||||
// This AzureClient instance connects to a local Tinylicious
|
||||
// instance rather than a live Azure Fluid Relay
|
||||
|
@ -78,25 +78,27 @@ 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 user = { id: "userId", name: "Test User" };
|
||||
const useAzure = process.env.FLUID_CLIENT === "azure";
|
||||
const tenantKey = useAzure ? (process.env.FLUID_TENANTKEY as string) : "";
|
||||
const user = { id: "userId", name: "Test User" };
|
||||
|
||||
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",
|
||||
};
|
||||
return new AzureClient({ connection:connectionConfig });
|
||||
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",
|
||||
};
|
||||
return new AzureClient({ connection: connectionConfig });
|
||||
}
|
||||
```
|
||||
|
||||
Your test can then call this function to create a client object without concerning itself about the underlying service. This [mocha](https://mochajs.org/) test example creates the service client before running any tests, and uses the [uuid](https://github.com/uuidjs/uuid) package to generate a random `documentId` for each test. You can substitute other libraries or implementations. There is a single test that uses the service client to create a container which passes as long as no errors are thrown.
|
||||
Your test can then call this function to create a client object without concerning itself about the underlying service. This [mocha](https://mochajs.org/) test example creates the service client before running any tests, and uses the [uuid](https://github.com/uuidjs/uuid) package to generate a random `documentId` for each test. You can substitute other libraries or implementations. There is a single test that uses the service client to create a container which passes as long as no errors are thrown.
|
||||
|
||||
```typescript
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
@ -104,23 +106,22 @@ import { v4 as uuid } from "uuid";
|
|||
// ...
|
||||
|
||||
describe("ClientTest", () => {
|
||||
const client = createAzureClient();
|
||||
let documentId: string;
|
||||
beforeEach("initializeDocumentId", () => {
|
||||
documentId = uuid();
|
||||
});
|
||||
const client = createAzureClient();
|
||||
let documentId: string;
|
||||
beforeEach("initializeDocumentId", () => {
|
||||
documentId = uuid();
|
||||
});
|
||||
|
||||
it("can create Azure container successfully", async () => {
|
||||
const schema: ContainerSchema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap
|
||||
},
|
||||
};
|
||||
it("can create Azure container successfully", async () => {
|
||||
const schema: ContainerSchema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const containerAndServices = await client.createContainer(schema);
|
||||
});
|
||||
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,7 +10,7 @@ The fluid-framework package bundles a collection of Fluid Framework client libra
|
|||
The `fluid-framework` packages surfaces APIs from the following sub-packages.
|
||||
The following APIs can be referenced independently, but they can all be imported directly from the `fluid-framework` package:
|
||||
|
||||
- [@fluidframework/map](./map)
|
||||
- [@fluidframework/sequence](./sequence)
|
||||
- [@fluidframework/fluid-static](./fluid-static)
|
||||
- [@fluidframework/container-definitions](./container-definitions)
|
||||
- [@fluidframework/map](./map)
|
||||
- [@fluidframework/sequence](./sequence)
|
||||
- [@fluidframework/fluid-static](./fluid-static)
|
||||
- [@fluidframework/container-definitions](./container-definitions)
|
||||
|
|
|
@ -10,7 +10,8 @@ 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))
|
||||
|
||||
- [@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))
|
||||
|
||||
For more information on our service client implementations, see [Fluid Services](../deployment/service-options).
|
||||
|
|
|
@ -3,17 +3,16 @@ title: User presence and audience
|
|||
sidebar_position: 8
|
||||
---
|
||||
|
||||
The audience is the collection of users connected to a container. When your app creates a container using a service-specific client library, the app is provided with a service-specific audience object for that container as well. Your code can query the audience object for connected users and use that information to build rich and collaborative user presence features.
|
||||
The audience is the collection of users connected to a container. When your app creates a container using a service-specific client library, the app is provided with a service-specific audience object for that container as well. Your code can query the audience object for connected users and use that information to build rich and collaborative user presence features.
|
||||
|
||||
This document will explain how to use the audience APIs and then provide examples on how to use the audience to show user presence. For anything service-specific, the [Tinylicious](/docs/testing/tinylicious) Fluid service is used.
|
||||
This document will explain how to use the audience APIs and then provide examples on how to use the audience to show user presence. For anything service-specific, the [Tinylicious](/docs/testing/tinylicious) Fluid service is used.
|
||||
|
||||
## Working with the audience
|
||||
|
||||
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.
|
||||
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;
|
||||
```
|
||||
|
||||
|
@ -23,12 +22,12 @@ Audience members exist as `IMember` objects:
|
|||
|
||||
```typescript
|
||||
export interface IMember {
|
||||
id: string;
|
||||
connections: IConnection[];
|
||||
id: string;
|
||||
connections: IConnection[];
|
||||
}
|
||||
```
|
||||
|
||||
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,12 +37,11 @@ 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
|
||||
export interface TinyliciousMember extends IMember {
|
||||
userName: string;
|
||||
userName: string;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -104,7 +104,7 @@ For example, on the creating computer, you don't want to call `container.attach`
|
|||
```typescript
|
||||
// Code that runs only on a creating client.
|
||||
if (container.attachState !== AttachState.Attached) {
|
||||
await container.attach();
|
||||
await container.attach();
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -112,10 +112,11 @@ 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)) {
|
||||
await container.attach();
|
||||
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)) {
|
||||
// Disable editing.
|
||||
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,20 +161,20 @@ 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", () => {
|
||||
// All pending edits have been saved.
|
||||
// All pending edits have been 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", () => {
|
||||
// The container has pending changes that need to be acknowledged by the service.
|
||||
// The container has pending changes that need to be acknowledged by the service.
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -272,31 +273,31 @@ Your code can disable the editing UI in your application when the container is d
|
|||
|
||||
```typescript
|
||||
container.on("disconnected", () => {
|
||||
// Prevent user edits to disable data loss.
|
||||
// Prevent user edits to disable data loss.
|
||||
});
|
||||
|
||||
container.on("connected", () => {
|
||||
// Enable editing if disabled.
|
||||
// Enable editing if disabled.
|
||||
});
|
||||
```
|
||||
|
||||
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", () => {
|
||||
// Disconnect the container when the user is idle.
|
||||
container.disconnect();
|
||||
// Disconnect the container when the user is idle.
|
||||
container.disconnect();
|
||||
});
|
||||
|
||||
user.on("active", () => {
|
||||
// Connect the container when the user is active again.
|
||||
container.connect();
|
||||
// Connect the container when the user is active again.
|
||||
container.connect();
|
||||
});
|
||||
```
|
||||
|
||||
### 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,12 +312,12 @@ 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
|
||||
container.on("disposed", () => {
|
||||
// Handle event cleanup to prevent memory leaks.
|
||||
// Handle event cleanup to prevent memory leaks.
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
@ -51,11 +49,11 @@ This example schema defines two initial objects, `layout` and `text`, and declar
|
|||
|
||||
```typescript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
text: SharedString
|
||||
},
|
||||
dynamicObjectTypes: [ SharedCell, SharedString ],
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
text: SharedString,
|
||||
},
|
||||
dynamicObjectTypes: [SharedCell, SharedString],
|
||||
};
|
||||
```
|
||||
|
||||
|
@ -65,13 +63,12 @@ Containers are created by passing the schema to the service-specific client's `c
|
|||
|
||||
```typescript {linenos=inline,hl_lines=[7,8]}
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } =
|
||||
await client.createContainer(schema);
|
||||
const { container, services } = await client.createContainer(schema);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
@ -100,13 +97,12 @@ Note that once published, a container cannot be unpublished. (But it can be dele
|
|||
|
||||
```typescript {linenos=inline,hl_lines=[10]}
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
initialObjects: {
|
||||
layout: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
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.
|
||||
|
@ -28,11 +28,11 @@ About this code note:
|
|||
|
||||
```typescript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
"custom-cell": SharedCell,
|
||||
}
|
||||
}
|
||||
initialObjects: {
|
||||
"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,
|
||||
|
@ -122,7 +122,7 @@ recalculate a derived value when some data in a DDS changes.
|
|||
|
||||
```ts
|
||||
myMap.on("valueChanged", () => {
|
||||
recalculate();
|
||||
recalculate();
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -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.
|
||||
|
@ -144,24 +144,24 @@ Let's assume for a moment that all of the data about a shape is stored as a sing
|
|||
|
||||
```json
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
{
|
||||
"aShape": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
}
|
||||
"aShape": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"height": 60,
|
||||
"width": 40
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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).
|
||||
|
||||
|
@ -14,20 +13,20 @@ This section covers how to consume and use Fluid handles.
|
|||
## Why 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:
|
||||
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
|
||||
Fluid runtime to retrieve the object.
|
||||
This reduces the complexity from the caller by abstracting away the need to know how to make a `request` to the
|
||||
Fluid runtime to retrieve the object.
|
||||
|
||||
- Handles enable the underlying Fluid runtime to build a dependency hierarchy. This will enable us to add garbage
|
||||
collection to the runtime in a future version.
|
||||
collection to the runtime in a future version.
|
||||
|
||||
## Basic Scenario
|
||||
|
||||
|
@ -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.
|
||||
|
@ -34,9 +33,9 @@ Here is a look at how you would go about loading `Signaler` as part of the initi
|
|||
|
||||
```typescript
|
||||
const containerSchema: ContainerSchema = {
|
||||
initialObjects: {
|
||||
signaler: Signaler,
|
||||
},
|
||||
initialObjects: {
|
||||
signaler: Signaler,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } = await client.createContainer(containerSchema);
|
||||
|
@ -60,7 +59,7 @@ 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";
|
||||
|
@ -68,7 +67,7 @@ private static readonly focusRequestType = "focusRequest";
|
|||
|
||||
```typescript
|
||||
container.on("connected", () => {
|
||||
this.signaler.submitSignal(FocusTracker.focusRequestType);
|
||||
this.signaler.submitSignal(FocusTracker.focusRequestType);
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -76,11 +75,11 @@ The connected clients are listening to this focus request signal, and they respo
|
|||
|
||||
```typescript
|
||||
this.signaler.onSignal(FocusTracker.focusRequestType, () => {
|
||||
this.sendFocusSignal(document.hasFocus());
|
||||
this.sendFocusSignal(document.hasFocus());
|
||||
});
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
@ -88,36 +87,36 @@ Rather than submitting multiple signals in response to an event, it is more cost
|
|||
|
||||
```typescript
|
||||
container.on("connected", () => {
|
||||
this.signaler.submitSignal("colorRequest");
|
||||
this.signaler.submitSignal("focusRequest");
|
||||
this.signaler.submitSignal("currentlySelectedObjectRequest");
|
||||
this.signaler.submitSignal("colorRequest");
|
||||
this.signaler.submitSignal("focusRequest");
|
||||
this.signaler.submitSignal("currentlySelectedObjectRequest");
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
this.signaler.onSignal("colorRequest", (clientId, local, payload) => {
|
||||
/*...*/
|
||||
/*...*/
|
||||
});
|
||||
this.signaler.onSignal("focusRequest", (clientId, local, payload) => {
|
||||
/*...*/
|
||||
/*...*/
|
||||
});
|
||||
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", () => {
|
||||
this.signaler.submitSignal("connectRequest");
|
||||
this.signaler.submitSignal("connectRequest");
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
this.signaler.onSignal("connectRequest", (clientId, local, payload) => {
|
||||
/*...*/
|
||||
/*...*/
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
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)).
|
||||
- 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)).
|
||||
|
||||
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.
|
||||
If you store class instances, for example, then data synchronization will not work as expected.
|
||||
- _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).
|
||||
|
@ -48,10 +49,10 @@ The following example loads a `SharedMap` as part of the initial roster of objec
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
}
|
||||
}
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } = await client.createContainer(schema);
|
||||
|
||||
|
@ -64,10 +65,10 @@ Similarly, if you are loading an existing container, the process stays largely i
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
}
|
||||
}
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const { container, services } = await client.getContainer(id, schema);
|
||||
|
||||
|
@ -103,7 +104,7 @@ Each edit will also trigger a `valueChanged` event which will be discussed in th
|
|||
- `entries()` -- Returns an iterator for all key/value pairs stored in the map
|
||||
- `delete(key)` -- Removes the key/value pair from the map
|
||||
- `forEach(callbackFn: (value, key, map) => void)` -- Applies the provided function to each entry in the map.
|
||||
For example, the following will print out all of the key/value pairs in the map
|
||||
For example, the following will print out all of the key/value pairs in the map
|
||||
|
||||
```javascript
|
||||
this.map.forEach((value, key) => console.log(`${key}-${value}`));
|
||||
|
@ -133,23 +134,23 @@ 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', () =>
|
||||
// Set the new value on the SharedMap
|
||||
map.set(dataKey, Math.random())
|
||||
button.addEventListener("click", () =>
|
||||
// Set the new value on the SharedMap
|
||||
map.set(dataKey, Math.random()),
|
||||
);
|
||||
|
||||
// This function will update the label from the SharedMap.
|
||||
// It is connected to the SharedMap's valueChanged event,
|
||||
// and will be called each time a value in the SharedMap is changed.
|
||||
const updateLabel = () => {
|
||||
const value = map.get(dataKey) || 0;
|
||||
label.textContent = `${value}`;
|
||||
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,23 +163,21 @@ 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) => {
|
||||
const value = map.get(dataKey) || 0;
|
||||
label.textContent = `${value} from ${local ? "me" : "someone else"}`;
|
||||
label.style.color = changed?.previousValue > value ? "red" : "green";
|
||||
const value = map.get(dataKey) || 0;
|
||||
label.textContent = `${value} from ${local ? "me" : "someone else"}`;
|
||||
label.style.color = changed?.previousValue > value ? "red" : "green";
|
||||
};
|
||||
updateLabel(undefined, false);
|
||||
// Use the changed event to trigger the rerender whenever the value changes.
|
||||
map.on('valueChanged', updateLabel);
|
||||
// Use the changed event to trigger the rerender whenever the value changes.
|
||||
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.
|
||||
|
@ -208,9 +207,9 @@ Here, each person may have multiple fields such as
|
|||
|
||||
```json
|
||||
{
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -218,33 +217,33 @@ And each task may also have multiple fields, including the person that it is ass
|
|||
|
||||
```json
|
||||
{
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -260,18 +259,18 @@ You can store each of these values in their own key and only hold the key at whi
|
|||
|
||||
```json
|
||||
{
|
||||
"task1": {
|
||||
"titleKey": "task1Title",
|
||||
"descriptionKey": "task1Description",
|
||||
"assignedToKey": "task1AssignedTo"
|
||||
},
|
||||
"task1Title": "Awesome Task",
|
||||
"task1Description": "Doing the most awesome things",
|
||||
"task1AssignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
"task1": {
|
||||
"titleKey": "task1Title",
|
||||
"descriptionKey": "task1Description",
|
||||
"assignedToKey": "task1AssignedTo"
|
||||
},
|
||||
"task1Title": "Awesome Task",
|
||||
"task1Description": "Doing the most awesome things",
|
||||
"task1AssignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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.
|
||||
|
@ -327,11 +326,11 @@ The following example demonstrates nesting DDSes using `SharedMap`. You specify
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
initialMap: SharedMap,
|
||||
},
|
||||
dynamicObjectTypes: [SharedMap]
|
||||
}
|
||||
initialObjects: {
|
||||
initialMap: 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.
|
||||
|
@ -370,24 +369,24 @@ You can further extend the example from the [Storing objects](#storing-objects)
|
|||
|
||||
```json
|
||||
{
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
},
|
||||
"task2": {
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
}
|
||||
"task1": {
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
},
|
||||
"task2": {
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -404,13 +403,13 @@ And the `task1` map would look like:
|
|||
|
||||
```json
|
||||
{
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
"title": "Awesome Task",
|
||||
"description": "Doing the most awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Joe Schmo",
|
||||
"email": "joeschmo@email.com",
|
||||
"address": "1234 Fluid Way"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -418,13 +417,13 @@ And the `task2` map would look like:
|
|||
|
||||
```json
|
||||
{
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
"title": "Even More Awesome Task",
|
||||
"description": "Doing even more awesome things",
|
||||
"assignedTo": {
|
||||
"name": "Jane Doe",
|
||||
"email": "janedoe@email.com",
|
||||
"address": "5678 Framework Street"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -434,11 +433,11 @@ Whenever a new task is created, you can call `container.create` to create a new
|
|||
|
||||
```javascript
|
||||
const schema = {
|
||||
initialObjects: {
|
||||
initialMap: SharedMap,
|
||||
},
|
||||
dynamicObjectTypes: [SharedMap]
|
||||
}
|
||||
initialObjects: {
|
||||
initialMap: 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,9 +32,9 @@ 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
|
||||
consensus process.
|
||||
- **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.
|
||||
|
||||
Below we've enumerated the data structures and described when they may be most useful.
|
||||
|
@ -48,8 +48,8 @@ Although the value of a pair can be a complex object, the value of any given pai
|
|||
### Common Problems
|
||||
|
||||
- Storing a lot of data in one key-value entry may cause performance or merge issues.
|
||||
Each update will update the entire value rather than merging two updates.
|
||||
Try splitting the data across multiple keys.
|
||||
Each update will update the entire value rather than merging two updates.
|
||||
Try splitting the data across multiple keys.
|
||||
- Storing arrays, lists, or logs in a single key-value entry may lead to unexpected behavior because users can't collaboratively modify parts of one entry.
|
||||
|
||||
## SharedString
|
||||
|
|
|
@ -24,32 +24,31 @@ matching the length of the text content they contain.
|
|||
Marker segments are never split or merged, and always have a length of 1.
|
||||
|
||||
```typescript
|
||||
// content: hi
|
||||
// positions: 01
|
||||
// content: hi
|
||||
// positions: 01
|
||||
|
||||
sharedString.insertMarker(
|
||||
2,
|
||||
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" }
|
||||
);
|
||||
sharedString.insertText(3, "world");
|
||||
// content: hi<pg marker>world
|
||||
// positions: 01 2 34567
|
||||
sharedString.insertMarker(
|
||||
2,
|
||||
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" },
|
||||
);
|
||||
sharedString.insertText(3, "world");
|
||||
// content: hi<pg marker>world
|
||||
// positions: 01 2 34567
|
||||
|
||||
// Since markers don't directly correspond to text, they aren't included in direct text queries
|
||||
sharedString.getText(); // returns "hiworld"
|
||||
|
||||
// Instead, rich text models involving markers likely want to read from the SharedString using `walkSegments`:
|
||||
sharedString.walkSegments((segment) => {
|
||||
if (Marker.is(segment)) {
|
||||
// Handle markers (e.g. dereference and insert image data, interpret marker properties, etc.)
|
||||
} else {
|
||||
// Handle text segments (e.g. append to the current text accumulator with `segment.text`, apply any formatting on `segment.props`)
|
||||
}
|
||||
});
|
||||
// Since markers don't directly correspond to text, they aren't included in direct text queries
|
||||
sharedString.getText(); // returns "hiworld"
|
||||
|
||||
// Instead, rich text models involving markers likely want to read from the SharedString using `walkSegments`:
|
||||
sharedString.walkSegments((segment) => {
|
||||
if (Marker.is(segment)) {
|
||||
// Handle markers (e.g. dereference and insert image data, interpret marker properties, etc.)
|
||||
} else {
|
||||
// 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.
|
||||
|
@ -194,7 +194,7 @@ 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).
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -100,7 +113,7 @@ The AzureClient connection config has been changed to have a single endpoint ins
|
|||
|
||||
If you're using the Public Preview of Azure Fluid Relay, use the following region dependent url as your new single endpoint url:
|
||||
|
||||
- West US 2 -> `https://us.fluidrelay.azure.com`
|
||||
- West US 2 -> `https://us.fluidrelay.azure.com`
|
||||
- West Europe -> `https://eu.fluidrelay.azure.com`
|
||||
- Southeast Asia -> `https://global.fluidrelay.azure.com`
|
||||
|
||||
|
|
|
@ -22,9 +22,9 @@ localhost.
|
|||
To get started you need the following installed.
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
- <NodeVersions />
|
||||
- <NodeVersions />
|
||||
- Code editor
|
||||
- We recommend [Visual Studio Code](https://code.visualstudio.com/).
|
||||
- We recommend [Visual Studio Code](https://code.visualstudio.com/).
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
## Getting started
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -14,7 +14,7 @@ The demo app uses Fluid Framework 2.X.
|
|||
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.
|
||||
|
||||
<LegacyDiceRollerSample />
|
||||
<br/>
|
||||
<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 +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,15 +130,15 @@ diceTemplate.innerHTML = `
|
|||
<div class="dice"></div>
|
||||
<button class="roll"> Roll </button>
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
const renderDiceRoller = (dice, elem) => {
|
||||
elem.appendChild(template.content.cloneNode(true));
|
||||
|
||||
const rollButton = elem.querySelector(".roll");
|
||||
const diceElem = elem.querySelector(".dice");
|
||||
|
||||
/* REMAINDER OF THE FUNCTION IS DESCRIBED BELOW */
|
||||
}
|
||||
/* REMAINDER OF THE FUNCTION IS DESCRIBED BELOW */
|
||||
};
|
||||
```
|
||||
|
||||
## Connect the view to Fluid data
|
||||
|
@ -148,14 +147,14 @@ Let's go through the rest of the `renderDiceRoller` function line-by-line.
|
|||
|
||||
### Create the Roll button handler
|
||||
|
||||
The next line of the `renderDiceRoller` function assigns a handler to the click event of the "Roll" button. Instead of updating the local state directly, the button updates the number stored in the `value` property of the `dice` object. Because `dice` is the root object of Fluid `SharedTree`, changes will be distributed to all clients. Any changes to `dice` will cause a `afterChanged` event to be emitted, and an event handler, defined below, can trigger an update of the view.
|
||||
The next line of the `renderDiceRoller` function assigns a handler to the click event of the "Roll" button. Instead of updating the local state directly, the button updates the number stored in the `value` property of the `dice` object. Because `dice` is the root object of Fluid `SharedTree`, changes will be distributed to all clients. Any changes to `dice` will cause a `afterChanged` event to be emitted, and an event handler, defined below, can trigger an update of the view.
|
||||
|
||||
This pattern is common in Fluid because it enables the view to behave the same way for both local and remote changes.
|
||||
|
||||
```js
|
||||
rollButton.onclick = () => {
|
||||
dice.value = Math.floor(Math.random() * 6) + 1;
|
||||
}
|
||||
rollButton.onclick = () => {
|
||||
dice.value = Math.floor(Math.random() * 6) + 1;
|
||||
};
|
||||
```
|
||||
|
||||
### Relying on Fluid data
|
||||
|
@ -165,15 +164,15 @@ 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 = () => {
|
||||
const diceValue = dice.value;
|
||||
// Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅)
|
||||
diceElem.textContent = String.fromCodePoint(0x267f + diceValue);
|
||||
diceElem.style.color = `hsl(${diceValue * 60}, 70%, 30%)`;
|
||||
}
|
||||
const updateDice = () => {
|
||||
const diceValue = dice.value;
|
||||
// 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
|
||||
|
@ -181,15 +180,15 @@ Note that the current value is retrieved from the `SharedMap` each time `updateD
|
|||
The next line ensures that the dice is rendered as soon as `renderDiceRoller` is called, which is when the container is created or loaded.
|
||||
|
||||
```js
|
||||
updateDice();
|
||||
updateDice();
|
||||
```
|
||||
|
||||
### 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);
|
||||
Tree.on(dice, "afterChange", updateDice);
|
||||
```
|
||||
|
||||
## Run the app
|
||||
|
|
|
@ -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
|
||||
|
@ -21,10 +21,10 @@ into the service client props. Both `createContainer()` and `getContainer()` met
|
|||
|
||||
```ts
|
||||
const loader = new Loader({
|
||||
urlResolver: this.urlResolver,
|
||||
documentServiceFactory: this.documentServiceFactory,
|
||||
codeLoader,
|
||||
logger: tinyliciousContainerConfig.logger,
|
||||
urlResolver: this.urlResolver,
|
||||
documentServiceFactory: this.documentServiceFactory,
|
||||
codeLoader,
|
||||
logger: tinyliciousContainerConfig.logger,
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
|
@ -42,15 +48,15 @@ The interface contains a `send()` method as shown:
|
|||
|
||||
```ts
|
||||
export interface ITelemetryBaseLogger {
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
}
|
||||
```
|
||||
|
||||
- `send()`
|
||||
- The `send()` method is called by the container's telemetry system whenever a telemetry event occurs. This method
|
||||
takes in an ITelemetryBaseEvent type parameter, which is also within the `@fluidframework/common-definitions`
|
||||
package. Given this method is part of an interface, users can implement a custom telemetry logic for the container's
|
||||
telemetry system to execute.
|
||||
takes in an ITelemetryBaseEvent type parameter, which is also within the `@fluidframework/common-definitions`
|
||||
package. Given this method is part of an interface, users can implement a custom telemetry logic for the container's
|
||||
telemetry system to execute.
|
||||
|
||||
### Customizing the logger object
|
||||
|
||||
|
@ -67,10 +73,10 @@ snippets below, or in the `@fluidframework/common-definitions` package for full
|
|||
```ts
|
||||
// @public
|
||||
export interface ITelemetryLogger extends ITelemetryBaseLogger {
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
sendErrorEvent(event: ITelemetryErrorEvent, error?: any): void;
|
||||
sendPerformanceEvent(event: ITelemetryPerformanceEvent, error?: any): void;
|
||||
sendTelemetryEvent(event: ITelemetryGenericEvent, error?: any): void;
|
||||
send(event: ITelemetryBaseEvent): void;
|
||||
sendErrorEvent(event: ITelemetryErrorEvent, error?: any): void;
|
||||
sendPerformanceEvent(event: ITelemetryPerformanceEvent, error?: any): void;
|
||||
sendTelemetryEvent(event: ITelemetryGenericEvent, error?: any): void;
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -107,17 +113,17 @@ required properties, `eventName` and `category`, are set by the telemetry system
|
|||
|
||||
```ts
|
||||
export interface ITelemetryBaseEvent extends ITelemetryProperties {
|
||||
category: string;
|
||||
eventName: string;
|
||||
category: string;
|
||||
eventName: string;
|
||||
}
|
||||
|
||||
export interface ITelemetryProperties {
|
||||
[index: string]: TelemetryEventPropertyType | ITaggedTelemetryPropertyType;
|
||||
[index: string]: TelemetryEventPropertyType | ITaggedTelemetryPropertyType;
|
||||
}
|
||||
|
||||
export interface ITaggedTelemetryPropertyType {
|
||||
value: TelemetryEventPropertyType,
|
||||
tag: string,
|
||||
value: TelemetryEventPropertyType;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export type TelemetryEventPropertyType = string | number | boolean | undefined;
|
||||
|
@ -132,7 +138,7 @@ either tagged (`ITaggedTelemetryPropertyType`) or untagged (`TelemetryEventPrope
|
|||
|
||||
Tags are strings used to classify the properties on telemetry events. By default, telemetry properties are untagged. However,
|
||||
the Fluid Framework may emit events with some properties tagged, so implementations of `ITelemetryBaseLogger` must be
|
||||
prepared to check for and interpret any tags. Generally speaking, when logging to the user's console, tags can
|
||||
prepared to check for and interpret any tags. Generally speaking, when logging to the user's console, tags can
|
||||
be ignored and tagged values logged plainly, but when transmitting tagged properties to a telemetry service,
|
||||
care should be taken to only log tagged properties where the tag is explicitly understood to indicate the value
|
||||
is safe to log from a data privacy standpoint.
|
||||
|
@ -143,15 +149,15 @@ The Fluid Framework sends events in the following categories:
|
|||
|
||||
- error -- used to identify and report error conditions, e.g. duplicate data store IDs.
|
||||
- performance -- used to track performance-critical code paths within the framework. For example, the summarizer tracks
|
||||
how long it takes to create or load a summary and reports this information in an event.
|
||||
how long it takes to create or load a summary and reports this information in an event.
|
||||
- generic -- used as a catchall for events that are informational and don't represent an activity with a duration like a
|
||||
performance event.
|
||||
performance event.
|
||||
|
||||
### 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
|
||||
events into different related cases. Once common example is `foo_start`, `foo_end` and `foo_cancel` for
|
||||
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.
|
||||
|
||||
### Customizing logged events
|
||||
|
@ -162,12 +168,12 @@ examples:
|
|||
|
||||
```ts
|
||||
if (chunk.version !== undefined) {
|
||||
logger.send({
|
||||
eventName: "MergeTreeChunk:serializeAsMinSupportedVersion",
|
||||
category: "generic",
|
||||
fromChunkVersion: chunk.version,
|
||||
toChunkVersion: undefined,
|
||||
});
|
||||
logger.send({
|
||||
eventName: "MergeTreeChunk:serializeAsMinSupportedVersion",
|
||||
category: "generic",
|
||||
fromChunkVersion: chunk.version,
|
||||
toChunkVersion: undefined,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -201,13 +207,15 @@ Here is the above telemetry event object in action:
|
|||
|
||||
```ts
|
||||
this.logger.sendTelemetryEvent({
|
||||
eventName: "connectedStateRejected",
|
||||
source,
|
||||
pendingClientId: this.pendingClientId,
|
||||
clientId: this.clientId,
|
||||
hasTimer: this.prevClientLeftTimer.hasTimer,
|
||||
inQuorum: protocolHandler !== undefined && this.pendingClientId !== undefined
|
||||
&& protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
|
||||
eventName: "connectedStateRejected",
|
||||
source,
|
||||
pendingClientId: this.pendingClientId,
|
||||
clientId: this.clientId,
|
||||
hasTimer: this.prevClientLeftTimer.hasTimer,
|
||||
inQuorum:
|
||||
protocolHandler !== undefined &&
|
||||
this.pendingClientId !== undefined &&
|
||||
protocolHandler.quorum.getMember(this.pendingClientId) !== undefined,
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -227,10 +235,10 @@ import { ITelemetryBaseLogger, ITelemetryBaseEvent } from "@fluidframework/core-
|
|||
// Define a custom ITelemetry Logger. This logger will be passed into TinyliciousClient
|
||||
// and gets hooked up to the Tinylicious container telemetry system.
|
||||
export class ConsoleLogger implements ITelemetryBaseLogger {
|
||||
constructor() {}
|
||||
send(event: ITelemetryBaseEvent) {
|
||||
console.log("Custom telemetry object array: ".concat(JSON.stringify(event)));
|
||||
}
|
||||
constructor() {}
|
||||
send(event: ITelemetryBaseEvent) {
|
||||
console.log("Custom telemetry object array: ".concat(JSON.stringify(event)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -8,13 +8,13 @@ slug: /testing/testing
|
|||
|
||||
## Overview
|
||||
|
||||
Testing and automation are crucial to maintaining the quality and longevity of your code. Internally, Fluid has a range of unit and integration tests powered by [Mocha](https://mochajs.org/), [Jest](https://jestjs.io/), [Puppeteer](https://github.com/puppeteer/puppeteer), and [webpack](https://webpack.js.org/). Tests that need to run against a service are backed by [Tinylicious](./tinylicious) or a test tenant of a [live service](../deployment/service-options) such as [Azure Fluid Relay](../deployment/azure-frs).
|
||||
Testing and automation are crucial to maintaining the quality and longevity of your code. Internally, Fluid has a range of unit and integration tests powered by [Mocha](https://mochajs.org/), [Jest](https://jestjs.io/), [Puppeteer](https://github.com/puppeteer/puppeteer), and [webpack](https://webpack.js.org/). Tests that need to run against a service are backed by [Tinylicious](./tinylicious) or a test tenant of a [live service](../deployment/service-options) such as [Azure Fluid Relay](../deployment/azure-frs).
|
||||
|
||||
This document will explain how to use these tools to get started with writing automation for Fluid applications against a service. It will focus on interactions with the service rather than automation in general, and will not cover the automation tools themselves or scenarios that do not require a service.
|
||||
This document will explain how to use these tools to get started with writing automation for Fluid applications against a service. It will focus on interactions with the service rather than automation in general, and will not cover the automation tools themselves or scenarios that do not require a service.
|
||||
|
||||
## Automation against Tinylicious
|
||||
|
||||
Automation against Tinylicious is useful for scenarios such as merge validation which want to be unaffected by service interruptions. Your automation should be responsible for starting a local instance of Tinylicious along with terminating it once tests have completed. This example uses the [start-server-and-test package](https://github.com/bahmutov/start-server-and-test) to do this. You can substitute other libraries or implementations.
|
||||
Automation against Tinylicious is useful for scenarios such as merge validation which want to be unaffected by service interruptions. Your automation should be responsible for starting a local instance of Tinylicious along with terminating it once tests have completed. This example uses the [start-server-and-test package](https://github.com/bahmutov/start-server-and-test) to do this. You can substitute other libraries or implementations.
|
||||
|
||||
First install the packages or add them to your dependencies then install:
|
||||
|
||||
|
@ -34,7 +34,7 @@ Once installed, you can use the following npm scripts:
|
|||
}
|
||||
```
|
||||
|
||||
The `test:tinylicious` script will start Tinylicious, wait until port 7070 responds (the default port on which Tinylicious runs), run the test script, and then terminate Tinylicious. Your tests can then use `TinyliciousClient` as usual (see [Tinylicious](./tinylicious)).
|
||||
The `test:tinylicious` script will start Tinylicious, wait until port 7070 responds (the default port on which Tinylicious runs), run the test script, and then terminate Tinylicious. Your tests can then use `TinyliciousClient` as usual (see [Tinylicious](./tinylicious)).
|
||||
|
||||
## Automation against Azure Fluid Relay
|
||||
|
||||
|
@ -42,26 +42,26 @@ Your automation can connect to a test tenant for Azure Fluid Relay in the same w
|
|||
|
||||
### Azure Fluid Relay as an abstraction for Tinylicious
|
||||
|
||||
The Azure Fluid Relay client can also connect to a local Tinylicious instance. This allows you to use a single client type between tests against live and local service instances, where the only difference is the configuration used to create the client.
|
||||
The Azure Fluid Relay client can also connect to a local Tinylicious instance. This allows you to use a single client type between tests against live and local service instances, where the only difference is the configuration used to create the client.
|
||||
|
||||
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 = {
|
||||
id: "UserId",
|
||||
name: "Test User",
|
||||
id: "UserId",
|
||||
name: "Test User",
|
||||
};
|
||||
const config = {
|
||||
type: "local",
|
||||
tokenProvider: new InsecureTokenProvider("fooBar", user),
|
||||
endpoint: "http://localhost:7070",
|
||||
type: "local",
|
||||
tokenProvider: new InsecureTokenProvider("fooBar", user),
|
||||
endpoint: "http://localhost:7070",
|
||||
};
|
||||
|
||||
const clientProps = {
|
||||
connection: config,
|
||||
}
|
||||
connection: config,
|
||||
};
|
||||
|
||||
// This AzureClient instance connects to a local Tinylicious
|
||||
// instance rather than a live Azure Fluid Relay
|
||||
|
@ -78,25 +78,27 @@ 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 user = { id: "userId", name: "Test User" };
|
||||
const useAzure = process.env.FLUID_CLIENT === "azure";
|
||||
const tenantKey = useAzure ? (process.env.FLUID_TENANTKEY as string) : "";
|
||||
const user = { id: "userId", name: "Test User" };
|
||||
|
||||
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",
|
||||
};
|
||||
return new AzureClient({ connection:connectionConfig });
|
||||
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",
|
||||
};
|
||||
return new AzureClient({ connection: connectionConfig });
|
||||
}
|
||||
```
|
||||
|
||||
Your test can then call this function to create a client object without concerning itself about the underlying service. This [mocha](https://mochajs.org/) test example creates the service client before running any tests, and uses the [uuid](https://github.com/uuidjs/uuid) package to generate a random `documentId` for each test. You can substitute other libraries or implementations. There is a single test that uses the service client to create a container which passes as long as no errors are thrown.
|
||||
Your test can then call this function to create a client object without concerning itself about the underlying service. This [mocha](https://mochajs.org/) test example creates the service client before running any tests, and uses the [uuid](https://github.com/uuidjs/uuid) package to generate a random `documentId` for each test. You can substitute other libraries or implementations. There is a single test that uses the service client to create a container which passes as long as no errors are thrown.
|
||||
|
||||
```typescript
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
@ -104,23 +106,22 @@ import { v4 as uuid } from "uuid";
|
|||
// ...
|
||||
|
||||
describe("ClientTest", () => {
|
||||
const client = createAzureClient();
|
||||
let documentId: string;
|
||||
beforeEach("initializeDocumentId", () => {
|
||||
documentId = uuid();
|
||||
});
|
||||
const client = createAzureClient();
|
||||
let documentId: string;
|
||||
beforeEach("initializeDocumentId", () => {
|
||||
documentId = uuid();
|
||||
});
|
||||
|
||||
it("can create Azure container successfully", async () => {
|
||||
const schema: ContainerSchema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap
|
||||
},
|
||||
};
|
||||
it("can create Azure container successfully", async () => {
|
||||
const schema: ContainerSchema = {
|
||||
initialObjects: {
|
||||
customMap: SharedMap,
|
||||
},
|
||||
};
|
||||
|
||||
const containerAndServices = await client.createContainer(schema);
|
||||
});
|
||||
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" />
|
||||
|
|
Загрузка…
Ссылка в новой задаче