From 8e892e08c45eb35008a664b58a3cb8af06617c2a Mon Sep 17 00:00:00 2001 From: Vlad Sudzilouski Date: Wed, 12 Aug 2020 23:13:19 -0700 Subject: [PATCH] Second round of request handler changes (#3090) **Key changes:** 1. IFluidHandleContext no longer implements IFluidRouter. It's request() method is renamed to resolveHandle(). This reduces number of responsibilities for both interfaces and makes it clearer on the purpose of interfaces. Note: This causes DataStoreRuntime to have both request() & resolveHandle(). In next iteration, they will have different behaviors - DDS URIs will fail to resolve when request comes in through request(). I.e. it would be impossible to request DDS from data store unless data store handler implements such access. This is equivalent to making DDSs private on a data store class. 2. 'path' is removed from both IFluidHandle & IFluidRouter. It has been marked deprecated for many versions. 3. Introducing handleFromLegacyUri() for back compat. Using it in couple places in this change to scope amount of churn - all places where it's used needs revisit. More on that below. 4. Fixing how we refer to task manager ID and how we access task manager & agent scheduler. In many places code confuses data store ID vs. type (even though they are the same thing). Exposing task manager on IContainerRuntimeBase. More on future changes here below. 5. Vltava: changing registry creation pattern - hiding component type names, forcing everything to go through factories. Same pattern should be expanded to all of our examples, as name we use in registry not always matches type on a factory. - Also removing using package name as component type - that has pretty big impact on bundle sizes, and is backward compat hazard - type name can't change, which is not obvious looking at package.json. 6. Last edited is simplified - async load path is deprecated, with assumption that it is being used on a critical path of container loading, with detached container creation. 7. PureDataObject method names are busted due to renames for many users (i.e. componentInitializingFirstTime is still used in code in derived classes, when such method was renamed on base class). Follow up for # 3 & 4 above: Next step will be to pass data object dependencies to them directly through scoping mechanism on object creation, and start removing access to "globals" in container. The most obvious example why it's bad is PureDataObject.getService() & getMatchMakerContainerService() implementations. Consumers of those are likely not aware that generateContainerServicesRequestHandler() has to be used by container developer. In fact, getMatchMakerContainerService() is not used in our repo (other than UT)! Better approach is to pass these dependencies through local (to object) scope, and force fluid object implementation to grab and store such dependencies (using handles) within data object itself, for future use. PureDataObjectFactory.instantiateInstance() can validate required dependencies are present. PR #2950 takes a first step on this path. --- BREAKING.md | 5 +- docs/docs/aqueduct.md | 12 +- docs/docs/fluid-handles.md | 78 ++ docs/docs/hello-world.md | 6 +- docs/docs/visual-component.md | 725 ++++++++++++++++++ docs/tutorials/dice-roller-comments.tsx | 127 +++ docs/tutorials/dice-roller.md | 298 +++++++ docs/tutorials/dice-roller.ts | 68 ++ docs/tutorials/sudoku.md | 437 +++++++++++ .../data-objects/client-ui-lib/package.json | 1 + .../client-ui-lib/src/controls/flowView.ts | 20 +- .../multiview/container/src/container.tsx | 31 +- .../data-objects/shared-text/src/component.ts | 6 +- examples/data-objects/vltava/package.json | 1 + .../vltava/src/components/anchor/anchor.ts | 11 +- .../src/components/tabs/newTabButton.tsx | 3 +- .../vltava/src/components/vltava/dataModel.ts | 9 +- .../vltava/src/components/vltava/vltava.tsx | 4 +- .../match-maker/matchMaker.ts | 10 +- examples/data-objects/vltava/src/index.ts | 47 +- packages/framework/aqueduct/README.md | 12 +- .../aqueduct/src/data-objects/blobHandle.ts | 3 +- .../aqueduct/src/data-objects/dataObject.ts | 7 +- .../src/data-objects/pureDataObject.ts | 5 +- .../src/request-handlers/requestHandlers.ts | 4 +- .../aqueduct/src/test/defaultRoute.spec.ts | 13 +- .../last-edited-experimental/README.md | 16 +- .../src/lastEditedTrackerComponent.ts | 10 +- .../last-edited-experimental/src/setup.ts | 30 +- .../request-handler/src/requestHandlers.ts | 46 +- .../src/test/requestHandlers.spec.ts | 9 +- .../src/test/dependencyContainer.spec.ts | 4 +- .../loader/core-interfaces/src/handles.ts | 30 +- .../component-runtime/src/componentRuntime.ts | 6 +- .../src/containerRuntime.ts | 8 - ...leContext.ts => containerHandleContext.ts} | 10 +- .../container-runtime/src/containerRuntime.ts | 26 +- .../container-runtime/src/summarizerHandle.ts | 6 +- .../src/test/summarizerHandle.spec.ts | 11 +- .../runtime/datastore/src/dataStoreRuntime.ts | 4 + packages/runtime/datastore/src/fluidHandle.ts | 13 - .../runtime/runtime-definitions/src/agent.ts | 5 + .../src/componentContext.ts | 3 + .../src/remoteComponentHandle.ts | 2 +- .../runtime/runtime-utils/src/serializer.ts | 32 +- .../runtime/runtime-utils/src/test/utils.ts | 4 +- .../runtime/test-runtime-utils/src/mocks.ts | 4 + .../src/test/agentScheduler.spec.ts | 9 +- .../src/test/batching.spec.ts | 6 +- .../src/test/opsOnReconnect.spec.ts | 5 +- 50 files changed, 1975 insertions(+), 267 deletions(-) create mode 100644 docs/docs/fluid-handles.md create mode 100644 docs/docs/visual-component.md create mode 100644 docs/tutorials/dice-roller-comments.tsx create mode 100644 docs/tutorials/dice-roller.md create mode 100644 docs/tutorials/dice-roller.ts create mode 100644 docs/tutorials/sudoku.md rename packages/runtime/container-runtime/src/{dataStoreHandleContext.ts => containerHandleContext.ts} (80%) diff --git a/BREAKING.md b/BREAKING.md index 5c14efa9871f..2feb48532af1 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -42,7 +42,8 @@ Temporarily exposed on IContainerRuntimeBase. The intent is to remove it altoget ## getDataStore() APIs is removed IContainerRuntime.getDataStore() is removed. Only IContainerRuntime.getRootDataStore() is available to retrieve root data stores. -For couple versions we will allow retrieving non-root data stores using this API, but this functionality is temporary and will be removed soon. We will provide other tools to support backward compatibility. +For couple versions we will allow retrieving non-root data stores using this API, but this functionality is temporary and will be removed soon. +You can use handleFromLegacyUri() for creating handles from container-internal URIs (i.e., in format `/${dataStoreId}`) and resolving those containers to get to non-root data stores. Please note that this functionality is strictly added for legacy files! In future, not using handles to refer to content (and storing handles in DDSs) will result in such data stores not being reachable from roots, and thus garbage collected (deleted) from file. ### Package Renames As a follow up to the changes in 0.24 we are updating a number of package names @@ -312,7 +313,7 @@ getAbsoluteUrl on the container runtime and component context now returns `strin import { waitForAttach } from "@fluidframework/aqueduct"; -protected async componentHasInitialized() { +protected async hasInitialized() { waitForAttach(this.runtime) .then(async () => { const url = await this.context.getAbsoluteUrl(this.url); diff --git a/docs/docs/aqueduct.md b/docs/docs/aqueduct.md index 601ec9f0772b..104c5f0e5eea 100644 --- a/docs/docs/aqueduct.md +++ b/docs/docs/aqueduct.md @@ -51,20 +51,20 @@ structures: /** * Called the first time the component is initialized. */ -protected async componentInitializingFirstTime(): Promise { } +protected async initializingFirstTime(): Promise { } /** * Called every time *except* first time the component is initialized. */ -protected async componentInitializingFromExisting(): Promise { } +protected async initializingFromExisting(): Promise { } /** * Called every time the component is initialized after create or existing. */ -protected async componentHasInitialized(): Promise { } +protected async hasInitialized(): Promise { } ``` -#### componentInitializingFirstTime +#### initializingFirstTime ComponentInitializingFirstTime is called only once. It is executed only by the _first_ client to open the component and all work will resolve before the component is presented to any user. You should overload this method to perform @@ -74,7 +74,7 @@ The `root` SharedDirectory can be used in this method. The following is an example from the Badge component. ```ts{5,10,19} -protected async componentInitializingFirstTime() { +protected async initializingFirstTime() { // Create a cell to represent the Badge's current state const current = SharedCell.create(this.runtime); current.set(this.defaultOptions[0]); @@ -106,7 +106,7 @@ SharedDirectory. ::: -#### componentInitializingFromExisting +#### initializingFromExisting ::: danger TODO diff --git a/docs/docs/fluid-handles.md b/docs/docs/fluid-handles.md new file mode 100644 index 000000000000..54ed069b8b82 --- /dev/null +++ b/docs/docs/fluid-handles.md @@ -0,0 +1,78 @@ +# Fluid Handles + +A [Fluid Handle](../../packages/loader/core-interfaces/src/handles.ts) is a handle to a `fluid object`. It is +used to represent the object and has a function `get()` that returns the underlying object. Handles move the ownership +of retrieving a `fluid object` from the user of the object to the object itself. The handle can be passed around in the +system and anyone who has the handle can easily get the underlying object by simply calling `get()`. + +## Why use Fluid Handles? + +- You should **always** use handles to represent `fluid objects` and store them in a Distributed Data Structure (DDS). + This tells the runtime, and the storage, about the usage of the object and that it is referenced. The runtime / + storage can then manage the lifetime of the object, and perform important operations such as garbage collection. + Otherwise, if the object is not referenced by a handle, it will be garbage collected. + + The exception to this is when the object has to be handed off to an external entity. For example, when copy / pasting + an object, the `url` of the object should be handed off to the destination so that it can request the object from the + Loader or the Container. In this case, it is the responsiblity of the code doing so to manage the lifetime to this + object / url by storing the handle somewhere, so that the object is not garbage collected. + +- With handles, the user doesn't have to worry about how to get the underlying object since that itself can differ in + different scenarios. It is the responsibility of the handle to retrieve the object and return it. + + For example, the [handle](../../packages/runtime/component-runtime/src/componentHandle.ts) for a `SharedComponent` + simply returns the underlying object. But when this handle is stored in a DDS so that it is serialized and then + de-seriazlied in a remote client, it is represented by a [remote + handle](../../packages/runtime/runtime-utils/src/remoteComponentHandle.ts). The remote handle just has the absolute + url to the object and requests the object from the root and returns it. + +## How to create a handle? + +A handle's primary job is to be able to return the `fluid object` it is representing when `get` is called. So, it needs +to have access to the object either by directly storing it or by having a mechanism to retrieve it when asked. The +creation depends on the usage and the implementation. + +For example, it can be created with the absolute `url` of the object and a `routeContext` which knows how to get the +object via the `url`. When `get` is called, it can request the object from the `routeContext` by providing the `url`. +This is how the [remote handle](../../packages/runtime/runtime-utils/src/remoteComponentHandle.ts) retrieves the +underlying object. + +## Usage + +A handle should always be used to represent a fluid object. Following are couple of examples that outline the usage of +handles to retrieve the underlying object in different scenarios. + +### Basic usage scenario + +One of the basic usage of a Fluid Handle is when a client creates a `fluid object` and wants remote clients to be able +to retrieve and load it. It can store the handle to the object in a DDS and the remote client can retrieve the handle +and `get` the object. + +The following code snippet from the [Pond](../../components/examples/pond/src/index.tsx) Component demonstrates this. It +creates `Clicker` which is a SharedComponent during first time initialization and stores its `handle` in the `root` DDS. +Any remote client can retrieve the `handle` from the `root` DDS and get `Clicker` by calling `get()` on the handle: + +```typescript +protected async initializingFirstTime() { + // The first client creates `Clicker` and stores the handle in the `root` DDS. + const clickerComponent = await Clicker.getFactory().createComponent(this.context); + this.root.set(Clicker.ComponentName, clickerComponent.handle); +} + +protected async hasInitialized() { + // The remote clients retrieve the handle from the `root` DDS and get the `Clicker`. + const clicker = await this.root.get(Clicker.ComponentName).get(); + this.clickerView = new HTMLViewAdapter(clicker); +} +``` + +### A more complex scenario + +Consider a scenario where there are multiple `Containers` and a `fluid object` wants to load another `fluid object`. + +If the `request-response` model is used to acheive this, to `request` the object using its `url`, the object loading it +has to know which `Container` has this object so that it doesn't end up requesting it from the wrong one. It can become +real complicated real fast as the number of `Components` and `Containers` grow. + +This is where Compponent Handles becomes really powerful and make this scenario much simpler. You can pass around the +`handle` to the `fluid object` across `Containers` and to load it from anywhere, you just have to call `get()` on it. diff --git a/docs/docs/hello-world.md b/docs/docs/hello-world.md index 257a62123423..8ef878942091 100644 --- a/docs/docs/hello-world.md +++ b/docs/docs/hello-world.md @@ -144,17 +144,17 @@ use the `root` `SharedDirectory`. ```typescript /** - * componentInitializingFirstTime is called only once, it is executed only by the first client to open the + * initializingFirstTime is called only once, it is executed only by the first client to open the * component and all work will resolve before the view is presented to any user. * * This method is used to perform component setup, which can include setting an initial schema or initial values. */ -protected async componentInitializingFirstTime() { +protected async initializingFirstTime() { this.root.set(diceValueKey, 1); } ``` -The `componentInitializingFirstTime` function is an override lifecycle method that is called the first time the +The `initializingFirstTime` function is an override lifecycle method that is called the first time the component instance is ever created. This is our opportunity to perform setup work that will only ever be run once. diff --git a/docs/docs/visual-component.md b/docs/docs/visual-component.md new file mode 100644 index 000000000000..641dd212a0c8 --- /dev/null +++ b/docs/docs/visual-component.md @@ -0,0 +1,725 @@ +# How to write a visual component + +## Introducing IComponentHtmlView + +All Fluid components expose their capabilities using the `IComponentX` interface pattern. Please see [Feature detection +and delegation](./components.md#feature-detection-and-delegation) for more information on this. + +As such, any component that provides a view -- that is, a component that is presented to the user visually -- exposes +this capability by implementing the `IComponentHTMLView` interface provided by the Fluid Framework. Let's take a look at +what this interface needs: + +```typescript +/** + * An IComponentHTMLView is a renderable component + */ +export interface IComponentHTMLView extends IProvideComponentHTMLView { + /** + * Render the component into an HTML element. + */ + render(elm: HTMLElement, options?: IComponentHTMLOptions): void; + + /** + * Views which need to perform cleanup (e.g. remove event listeners, timers, etc.) when + * removed from the DOM should implement remove() and perform that cleanup within. + */ + remove?(): void; +} + +export interface IProvideComponentHTMLView { + readonly IComponentHTMLView: IComponentHTMLView; +} +``` + +As we can see, the two functions that we must implement are the `IComponentHTMLView` identifier and a `render(elm: +HTMLElement)` function. `remove()` is not mandatory and only necessary for clean up operations when the view is being +removed. + +- `IComponentHTMLView` can simply provide itself as `this` to identify that this component itself is a view provider. + With Fluid, each component uses the identifiers to expose their capabilities and are anonymous interfaces otherwise. + As such, another component (`componentB`) that does not know if this component (`componentA`) provides a view but can + check by seeing if `componentA.IComponentHTMLView` is defined or not. If `componentA.IComponentHTMLView` is defined, + it is guaranteed to return a `IComponentHTMLView` object. At this point, it can render `componentA` by calling + `componentA.IComponentHTMLView.render()`. This may seem initially confusing but the example below should demonstrate + its ease of implementation and you can read [here](../docs/components.md#feature-detection-and-delegation) for more + reference. +- `render` is a function that takes in the parent HTML document element and allows children to inject their views into + it. The `elm` parameter passed in here can be modified and returned. If you are using React as your view framework, + this is where you would pass the `elm` to the `ReactDOM.render` function to start rendering React components + +```typescript +public render(elm: HTMLElement) { + ReactDOM.render( + , + elm, + ); + } + ``` + + +To see an example of how a Fluid component can implement this interface, we will be looking at a simple `Clicker` +component that consists of a label with a number starting at `0` and a button that increments the displayed number for +all users simultaneously. + +Over the course of this example, we will incrementally build out our entire component in one file. The final two +versions of the code can be found at the bottom of each section. Each of these files define an entire, self-sufficient +Fluid component that can be exported in its own package. + +First, we will look at how `Clicker` is set up such that it defines itself as a Fluid Component and uses a shared state. +Then, we will take a look at two different ways of generating a view that responds to changes on that shared state when +a user presses a button: + +- Option A: Using a robust, event-driven pattern that can be emulated for different view frameworks. +- Option B: Using a React adapter that React developers may find more familiar but uses experimental code that is still being developed. + +## Setting Up the Fluid Component + +Before we do either of the options, we first need to do some common steps to say that `Clicker` is a Fluid component, +and bring in the necessary imports. Below is the initial code to get you started; we will incrementally build on this +throughout the example. The final result of this file is sufficient to produce a standalone component, and can be +supplied as the `main` script in your component `package.json`. + +Alright, lets take a look at some initial scaffolding. + +```typescript +import { PrimedComponent, PrimedComponentFactory } from "@fluidframework/aqueduct"; + +const ClickerName = "Clicker"; +export class Clicker extends PrimedComponent { +} + +export const ClickerInstantiationFactory = new PrimedComponentFactory( + ClickerName, + Clicker, + [/* SharedObject dependencies will go here */], + {}, +); +export const fluidExport = ClickerInstantiationFactory; + ``` + +Believe it or not, we are now 90% of the way to having a visual Fluid component! Do not worry if it doesn't compile yet. +Let's take a quick summary of what we've written though before going the last bit to attain compilation. + +```typescript +export class Clicker extends PrimedComponent +``` + +By extending the abstract `PrimedComponent` class, we have actually let it do a lot of the necessary set up work for us +through its constructor. Specifically, the `PrimedComponent` class gives us access to two items + +- `root`: The `root` is a `SharedDirectory` [object](../docs/SharedDirectory.md) which, as the name implies, is a + directory that is shared amongst all users that are rendering this component in the same session. Any items that are + set here on a key will be accessible to other users using the same key on their respective client's root. The stored + values can be primitives or the handles of other `SharedObject` items. If you don't know what handles are, don't + worry! We'll take a look at them in the next section. + +- `runtime`: The `runtime` is a `ComponentRuntime` object that manages the Fluid component lifecycle. The key thing to + note here is that it will be used for the creation of other Fluid components and DDS'. + +```typescript +export const ClickerInstantiationFactory = new PrimedComponentFactory( + ClickerName, + Clicker, + [/* SharedObject dependencies will go here */], + {}, +); +export const fluidExport = ClickerInstantiationFactory; +``` +These two lines in combination allow the Clicker component to be consumed as a Fluid component. While the first two +parameters that `PrimedComponentFactory` takes in simply define `Clicker`'s name and pass the class itself, the third +parameter is important to keep in mind for later as it will list the Fluid DDS' (Distributed Data Structures) that +`Clicker` utilizes. + +Finally, the last line consisting of an exported `fluidExport` variable is what Fluid containers look for in order to +instantiate this component using the factory it provides. + +Awesome, now that we're up to speed with our code scaffolding, let's add the actual counter data structure that we will +use to keep track of multiple users clicking the button, and a rudimentary render function. Following that, we will link +the two together. + +## Adding a SharedObject and a Basic View + +```typescript +import { PrimedComponent, PrimedComponentFactory } from "@fluidframework/aqueduct"; +import { SharedCounter } from "@fluidframework/counter"; +import { IComponentHTMLView } from "@fluidframework/view-interfaces"; +import React from "react"; +import ReactDOM from "react-dom"; + +const ClickerName = "Clicker"; + +export class Clicker extends PrimedComponent implements IComponentHTMLView { + public get IComponentHTMLView() { return this; } + + public render(elm: HTMLElement) { + ReactDOM.render( +
, + elm, + ); + return elm; + } +} + +export const ClickerInstantiationFactory = new PrimedComponentFactory( + ClickerName, + Clicker, + [SharedCounter.getFactory()], + {}, +); + +export const fluidExport = ClickerInstantiationFactory; + +``` + +Now, our clicker has a `SharedCounter` available, which is an extension of `SharedObject` and can provide a simple empty +view. Although a little light on functionality, by just adding those few lines, our `Clicker` is now a compiling, visual +component! We will add in our business logic in a second but first, let's see what these few lines achieved. + +As discussed above, the `PrimedComponent` already gives us a `SharedObject` in the form of the `SharedDirectory` root. +Any primitive we set to a key of the root, i.e. `root.set("key", 1)`, can be fetched by another user, i.e. +`root.get("key")` will return 1. However, different `SharedObject` classes have different ways of dictating merge logic, +so you should pick the one that best suits your needs given the scenario. + +Although we can simply set a number on the `root` and increment it on `Clicker` clicks, we will use a `SharedCounter` +[object](../docs/SharedCounter.md#creation) instead, as it handles scenarios where multiple users click at the same +time. We add that `SharedObject` to our `Clicker` by passing it as the third dependency in the factory constructor. We +only need to add it to this list once, even if we use multiple `SharedCounter` instances. + +```typescript +export const ClickerInstantiationFactory = new PrimedComponentFactory( + ClickerName, + Clicker, + [SharedCounter.getFactory()], + {}, +); +``` + +We will also be displaying our `Clicker` incrementing in a view, so we also need to mark our component as an +`IComponentHTMLView` component to say that it provides a render function, as explained at the beginning of this page. +This is done by adding the adding the `IComponentHTMLView` interface and implementing the first mandatory function, +`IComponentHTMLView`, by returning itself. + +```typescript +export class Clicker extends PrimedComponent implements IComponentHTMLView { + public get IComponentHTMLView() { return this; } +} +``` + +We then implement the second mandatory function, `render(elm: HTMLElement)`, by calling the `ReactDOM.render` function +and returning an empty div for now. + +```typescript +public render(elm: HTMLElement) { + ReactDOM.render( +
, + elm, + ); + return elm; +} +``` + +Now that we can start using the SharedCounter DDS and have labeled this component as one that provides a view, we can +start filling out the view itself and how it updates to changes in DDS'. + +## Creating the View + +Now, we have two choices for crafting our view. + +The recommended choice currently is to use event-driven views. This ties events that are fired on `SharedObject` changes +to trigger re-renders for any view framework. When using React, instead of needing to re-render, we can simply call +`setState` with the new value. + +There is a also a new, experimental Fluid React library that React developers may find easier to use since it abstracts +much of the event-driven state update logic, but it is a still a WiP for scenarios such as cross-component relationships +and may be unstable. However, we can use it for standalone components such as this. It is still recommended to read the +event-driven case even if you choose to apply the Fluid React libraries to understand the logic that is happening +beneath the abstraction the libraries provide. + +### OPTION A: Event-Driven Views + +#### Setting Up The Counter + +Now that we have all of our scaffolding ready, we can actually start adding in the logic of creating an instance of the +`SharedCounter` object. Let's take a look at what this entails. + +```typescript +import { PrimedComponent, PrimedComponentFactory } from "@fluidframework/aqueduct"; +import { IComponentHandle } from "@fluidframework/core-interfaces"; +import { SharedCounter } from "@fluidframework/counter"; +import { IComponentHTMLView } from "@fluidframework/view-interfaces"; +import React from "react"; +import ReactDOM from "react-dom"; + +const ClickerName = "Clicker"; +const counterKey = "counter"; + +export class Clicker extends PrimedComponent implements IComponentHTMLView { + public get IComponentHTMLView() { return this; } + + private _counter: SharedCounter | undefined; + + protected async initializingFirstTime() { + const counter = SharedCounter.create(this.runtime); + this.root.set(counterKey, counter.handle); + } + + protected async hasInitialized() { + const counterHandle = this.root.get>(counterKey); + this._counter = await counterHandle.get(); + } + + public render(div: HTMLElement) { + ReactDOM.render( +
, + div, + ); + return div; + } +} + +export const ClickerInstantiationFactory = new PrimedComponentFactory( + ClickerName, + Clicker, + [SharedCounter.getFactory()], + {}, +); + +export const fluidExport = ClickerInstantiationFactory; +``` + +A good way of understanding what is happening here is thinking about the two different scenarios this component will be rendered in. + +- This is the first time this component is being rendered in this session for any user, i.e. some user opened this + `Clicker` session for the first time +- This is another user who is joining an existing session and is rendering a component with data that has already been + updated, i.e. somebody clicked the `Clicker` a number of times already and now a new user enters to see the already + incremented value + +To cater to these two scenarios, the Fluid component lifecycle provides three different functions: + +- `initializingFirstTime` - This code will be run by clients in the first, new session scenario +- `initializingFromExisting` - This code will be run by clients in the second, existing session scenario +- `hasInitialized` - This code will be run by clients in both the new and existing session scenarios + +These all run prior to the first time `render` is called and can be async. As such, this is the perfect place to do any +setup work, such as assembling any DDS' you will need. + +With this knowledge, let's examine what Clicker is doing with these functions. + +```typescript +private _counter: SharedCounter | undefined; + +protected async initializingFirstTime() { + const counter = SharedCounter.create(this.runtime); + this.root.set(counterKey, counter.handle); +} +``` + +Since this is the first time this component has been rendered in this session, we need to add a `SharedCounter` to +everyone's shared `root` so we can all increment on the same object. We do this using the `SharedCounter.create` +function that simply takes in the `runtime` object that we have handy from the `PrimedComponent` class we inherited +earlier. + +This now gives us an instance of `SharedCounter` to play with and set to our class. If you try to inspect it, you will +see that it has a list of functions including `value` and `increment` that we will use later. + +However, it's not enough to just get an instance of `SharedCounter` ourselves! We need to make sure that any other +client that renders this also gets the same `SharedCounter`. Well, we know that we all share the same `root`, so we can +simply set it on a key there. + +While `counter` itself cannot be directly stored, it provides a `counter.handle` that can be. We store it in the root +under a key string `counterKey` using + +```typescript +this.root.set(counterKey, counter.handle); +``` + +So, this will ensure that there is always a `Counter` handle available in the root under that key. Now, let's take a +look at how to fetch it. + +```typescript +protected async hasInitialized() { + const counterHandle = this.root.get>(counterKey); + this._counter = await counterHandle.get(); +} +``` + +As we can see here, every client, whether its the first one or one joining an existing session will try to fetch a +handle from the `root` by looking under the `counterKey` key. Simply calling `await counterHandle.get()` now will give +us an instance of the same `SharedCounter` we had set in `initializingFirstTime`. + +In a nutshell, this means that `this._counter` is the same instance of `SharedCounter` for all of the clients that share +the same `root`. + +#### Creating the view + +Alright, now for the moment you've been waiting for, connecting the counter to the view. + +##### Final code + +```typescript +import { PrimedComponent, PrimedComponentFactory } from "@fluidframework/aqueduct"; +import { IComponentHandle } from "@fluidframework/core-interfaces"; +import { SharedCounter } from "@fluidframework/counter"; +import { IComponentHTMLView } from "@fluidframework/view-interfaces"; +import React from "react"; +import ReactDOM from "react-dom"; + +const ClickerName = "Clicker"; +const counterKey = "counter"; + +export class Clicker extends PrimedComponent implements IComponentHTMLView { + public get IComponentHTMLView() { return this; } + + private _counter: SharedCounter | undefined; + + protected async initializingFirstTime() { + const counter = SharedCounter.create(this.runtime); + this.root.set(counterKey, counter.handle); + } + + protected async hasInitialized() { + const counterHandle = this.root.get>(counterKey); + this._counter = await counterHandle.get(); + } + + public render(div: HTMLElement) { + if (this._counter === undefined) { + throw new Error("SharedCounter not initialized"); + } + ReactDOM.render( + , + div, + ); + return div; + } +} + +interface CounterProps { + counter: SharedCounter; +} + +interface CounterState { + value: number; +} + +class CounterReactView extends React.Component { + constructor(props: CounterProps) { + super(props); + + this.state = { + value: this.props.counter.value, + }; + } + + render() { + return ( +
+ + {this.state.value} + + +
+ ); + } + + componentDidMount() { + this.props.counter.on("incremented", (incrementValue: number, currentValue: number) => { + this.setState({ value: currentValue }); + }); + } +} + +export const ClickerInstantiationFactory = new PrimedComponentFactory( + ClickerName, + Clicker, + [SharedCounter.getFactory()], + {}, +); + +export const fluidExport = ClickerInstantiationFactory; +``` + +Let's walk through the changes here. First off, we have our new `render` function that actually supplies a useful value +now instead of an empty `div`. + +```typescript +public render(div: HTMLElement) { + if (this._counter === undefined) { + throw new Error("SharedCounter not initialized"); + } + ReactDOM.render( + , + div, + ); + return div; +} +``` + +By the time any client reaches the render function, they should have either created a `SharedCounter` and fetched it +from the root, or fetched an existing one from the root. As such, we will error if it's not available. Then we pass the +counter as a prop to our new React component `CounterReactView` inside `ReactDOM.render`. This will be your main entry +point into the React view lifecycle. Let's take a look at the `CounterReactView` component next. + +```typescript +interface CounterProps { + counter: SharedCounter; +} + +interface CounterState { + value: number; +} + +class CounterReactView extends React.Component { + constructor(props: CounterProps) { + super(props); + + this.state = { + value: this.props.counter.value, + }; + } +} +``` + +Here we can see that the `CounterReactView` takes in a `SharedCounter` as a prop and sets its initial state to its +value. This means that if one client incremented the `SharedCounter` four times from 0 to 4, the new client will see 4 +on its first render and continue incrementing from there. We'll examine the render next. + +```typescript +render() { + return ( +
+ + {this.state.value} + + +
+ ); +} +``` + +This has two interesting sections: + +- `this.state.value` - This is where we render the value that we set in our state in the constructor +- `this.props.counter.increment` - When the user presses the + button, it increments the SharedCounter object passed in + the props + +Now, the portion you will have noticed is missing is where the update on the `props.counter` translates to a +`state.value` update. This happens in the event listener we set up in `componentDidMount`. + +```typescript +componentDidMount() { + this.props.counter.on("incremented", (incrementValue: number, currentValue: number) => { + this.setState({ value: currentValue }); + }); +} +``` + +When we fire the `counter.increment` function, the `SharedCounter` emits a `"incremented"` event on all instances of it, +i.e. any client that is rendering it will receive this. The event carries the new counter value, and the callback simply +sets that value to the `state`. And there you have it! A synced counter! Any users who are rendering this `Clicker` will +be able to increment the counter together, and they can refresh their page and see that their count is persistent. + +While this may seem trivial, the patterns of listening to events emitted on DDS updates can be extended to any +`SharedObject` including [SharedString](../docs/SharedString.md), [SharedMap](../docs/SharedMap.md), etc. These can then +be hooked up to different React views, and UI callbacks on these views can then be piped into actions on Fluid DDS'. + +### OPTION B: React views + +::: warning + +The following code uses dependencies that are very experimental and may be unstable. + +::: + +Now we are going to take the scaffolding that we set up earlier and add in our React libraries to tie our synced state +update to our local React state update. If you read through Option A, you will see that the Fluid React libraries will +now handle much of the event-listening logic that we wrote earlier. + +##### Final code + +```typescript +import { + PrimedComponent, + PrimedComponentFactory, +} from "@fluidframework/aqueduct"; +import { + FluidReactComponent, + IFluidFunctionalComponentFluidState, + IFluidFunctionalComponentViewState, + FluidToViewMap, +} from "@fluidframework/react"; +import { SharedCounter } from "@fluidframework/counter"; +import { IComponentHTMLView } from "@fluidframework/view-interfaces"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +interface CounterState { + counter?: SharedCounter; +} + +type CounterViewState = IFluidFunctionalComponentViewState & CounterState; +type CounterFluidState = IFluidFunctionalComponentFluidState & CounterState; + + +export class Clicker extends PrimedComponent implements IComponentHTMLView { + public get IComponentHTMLView() { return this; } + + public render(element: HTMLElement) { + const fluidToView: FluidToViewMap = new Map(); + fluidToView.set("counter", { + sharedObjectCreate: SharedCounter.create, + listenedEvents: ["incremented"], + }); + + ReactDOM.render( + , + element, + ); + return element; + } +} + +class CounterReactView extends FluidReactComponent { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return ( +
+ + {this.state.counter?.value} + + +
+ ); + } +} + +export const ClickerInstantiationFactory = new PrimedComponentFactory( + ClickerName, + Clicker, + [SharedCounter.getFactory()], + {}, +); +export const fluidExport = ClickerInstantiationFactory; +``` + +Let's take this in parts to understand the link between the Fluid Component and the view that we establish here. + +First, let's just take a look at the interfaces our view will be using. + +```typescript +interface CounterState { + counter?: SharedCounter; +} + +type CounterViewState = IFluidFunctionalComponentViewState & CounterState; +type CounterFluidState = IFluidFunctionalComponentFluidState & CounterState; +``` + +The `CounterViewState` and `CounterFluidState` here both have the `counter` available. The former is what will be used +by our React views to render, whereas the latter is used for managing the synced state on our `root`. In the case of a +simple example like this, they are largely the same apart from extending from two different base classes provided by the +framework. They can be different in more involved examples where we want to abstract Fluid DDS objects out of the +interface consumed by our views, so that they can exist without requiring Fluid knowledge. + +Now, let us break down the `render` function to see how the relationship between these two states is set. + +```typescript +public render(element: HTMLElement) { + const fluidToView: FluidToViewMap = new Map(); + fluidToView.set("counter", { + sharedObjectCreate: SharedCounter.create, + listenedEvents: ["incremented"], + }); + + ReactDOM.render( + , + element, + ); + return element; +} +``` + +Here, we construct a `fluidToView` mapping to describe the relationship between `counter` in the view state and +`counter` in the Fluid state. If this is the first time this component is being rendered, it will use the callback in +`sharedObjectCreate` to initialize the `SharedCounter` object on the synced state. Any returning clients will +automatically fetch this stored value, convert it from a handle to the component itself, and pass it into the view +state. + +We also pass in the `listenedEvents` parameter to indicate which events on this Fluid state value should trigger a state +update. Here we pass in `"incremented"` as we want the view to refresh when the user increments. + +This also optionally takes in `stateKey`, `viewConverter`, and `rootKey` parameters to handle cases where the view and +fluid states do not match, but they are not needed here. + +If you read Option A above, you will notice that we no longer need to set up the `SharedCounter` in the component +lifecycle and that we only have the `render` function now. This is because this initialization is happening within the +React lifecycle, and the `SharedCounter` instance will be made available through a state update after it finishes +initializing. This is why you see that `CounterState` is defined as `{counter?: SharedCounter}` instead of `{counter: +SharedCounter}`. Prior to initialization, `state.counter` will return undefined. + +Okay, now we have everything necessary to pass in as props to our `CounterReactView`. + +- `syncedStateId` - This should be unique for each component that shares the same root, i.e. if there was another + clicker being render alongside this one in this component, it should receive its own ID to prevent one from + interfering in the updates of the other +- `root` - The same `SharedDirectory` provided by `this.root` from `PrimedComponent` +- `dataProps.fluidComponentMap` - This can just take a new `Map` instance for now but will need to be filled when + establishing multi-component relationships in more complex cases. This map is where all the DDS' that we use are + stored after being fetched from their handles, and it used to make the corresponding component synchronously available + in the view. +- `dataProps.runtime` - The same `ComponentRuntime` provided by `this.runtime` from `PrimedComponent` +- `fluidToView` - The fluidToView relationship map we set up above. + +We're ready to go through our view, which is now super simple due to the setup we did in the Fluid component itself. + +```typescript +class CounterReactView extends FluidReactComponent { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return ( +
+ + {this.state.counter?.value} + + +
+ ); + } +} +``` + +We can see that the state is initially empty as it only consists of the `SharedCounter` DDS, and we know the +`FluidReactComponent` will be handling the loading of that since we passed it as a key in the `fluidToView` map. + +The view itself can now directly use the `this.state.counter.value` and we can update it by simply using +`this.state.counter.increment(1)`. This will directly update the `this.state.counter.value` without needing any event +listeners to be additionally set up. And there you have it, a synced clicker with persistent state without needing to +directly use IComponentHandles or set up event listeners! + +We can extend this example to other DDS' by passing in their corresponding `create` functions in and listening to their +respective events. diff --git a/docs/tutorials/dice-roller-comments.tsx b/docs/tutorials/dice-roller-comments.tsx new file mode 100644 index 000000000000..a70c37381bc0 --- /dev/null +++ b/docs/tutorials/dice-roller-comments.tsx @@ -0,0 +1,127 @@ +import { + PrimedComponent, + PrimedComponentFactory, +} from "@fluidframework/aqueduct"; +import { IValueChanged } from "@fluidframework/map"; +import { IFluidHTMLView } from "@fluidframework/view-interfaces"; +import { EventEmitter } from "events"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +const diceValueKey = "diceValue"; + +/** + * Describes the public API surface for our Fluid component. + */ +export interface IDiceRoller extends EventEmitter { + /** + * Get the dice value as a number. + */ + readonly value: number; + + /** + * Roll the dice. Will cause a "diceRolled" event to be emitted. + */ + roll: () => void; + + /** + * The diceRolled event will fire whenever someone rolls the device, either locally or remotely. + */ + on(event: "diceRolled", listener: () => void): this; +} + +/** + * Fluid component + */ +export class DiceRoller extends PrimedComponent implements IDiceRoller, IFluidHTMLView { + public static get ComponentName() { + return "DiceRoller"; + } + + public get IFluidHTMLView() { return this; } + + /** + * The factory defines how to create an instance of the component as well as the + * dependencies of the component. + */ + public static readonly factory = new PrimedComponentFactory( + DiceRoller.ComponentName, + DiceRoller, + [], + {}, + ); + + /** + * initializingFirstTime is called only once, it is executed only by the first client to open the + * component and all work will resolve before the view is presented to any user. + * + * This method is used to perform component setup, which can include setting an initial schema or initial values. + */ + protected async initializingFirstTime() { + this.root.set(diceValueKey, 1); + } + + /** + * hasInitialized runs every time the component is initialized including the first time. + */ + protected async hasInitialized() { + this.root.on("valueChanged", (changed: IValueChanged) => { + if (changed.key === diceValueKey) { + this.emit("diceRolled"); + } + }); + } + + /** + * Render the dice. + */ + public render(div: HTMLElement) { + ReactDOM.render( + , + div, + ); + } + + public get value() { + return this.root.get(diceValueKey); + } + + public readonly roll = () => { + const rollValue = Math.floor(Math.random() * 6) + 1; + this.root.set(diceValueKey, rollValue); + }; +} + +interface IDiceRollerViewProps { + model: IDiceRoller; +} + +export const DiceRollerView: React.FC = (props: IDiceRollerViewProps) => { + const [diceValue, setDiceValue] = React.useState(props.model.value); + + React.useEffect(() => { + const onDiceRolled = () => { + setDiceValue(props.model.value); + }; + props.model.on("diceRolled", onDiceRolled); + return () => { + props.model.off("diceRolled", onDiceRolled); + }; + }, [props.model]); + + // Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅) + const diceChar = String.fromCodePoint(0x267F + diceValue); + + return ( +
+ { diceChar } + +
+ ); +}; + +/** + * Having a fluidExport that points to our factory allows for dynamic component + * loading. + */ +export const fluidExport = DiceRoller.factory; diff --git a/docs/tutorials/dice-roller.md b/docs/tutorials/dice-roller.md new file mode 100644 index 000000000000..5449a7642399 --- /dev/null +++ b/docs/tutorials/dice-roller.md @@ -0,0 +1,298 @@ +--- +title: Dice roller +sidebarDepth: 2 +--- + +::: danger + +OUTDATED + +::: + +The Dice roller is a simple Fluid component that uses Fluid's distributed data structures to simulate rolling a die. + +## Set up your dev environment + +If you haven't already, [set up your Fluid Framework development +environment](../guide/README.md#set-up-your-development-environment). + +::: danger TODO + +Update these instructions. + +::: + +First, clone one of the tutorial repositories below. One repository uses React for rendering, and the other uses +JavaScript directly. + +- [Dice roller tutorial - Vanilla JavaScript](https://github.com/microsoft/fluid-tutorial-dice-roller) +- [Dice roller tutorial - React](https://github.com/microsoft/fluid-tutorial-dice-roller-react) + +Once you've cloned the repo, you'll need to set up access to the [private Fluid npm feed](../guide/package-feed.md). On +Windows, you can run the `npm run auth` command to automate this process. + +Now that you have access to the private feed, run `npm install` in the root of the repository to install dependencies. + +Finally, you can open the folder in Visual Studio Code. + +## main.tsx + +The `src/fluid-components/index.tsx` file is where the component logic lives. + +### Declare imports + +First we will declare all our imports. Here is a quick description and use cases for each is discussed further below. + +`PrimedComponent` and `PrimedComponentFactory` from [@fluidframework/aqueduct](../api/aqueduct.md) provide helper +functionality. `IComponentHTMLView` from +[@fluidframework/core-interfaces](../api/core-interfaces.md) provides the interface for +enabling rendering. `React` and `ReactDOM` enable React use. + +```typescript +import { + PrimedComponent, + PrimedComponentFactory +} from "@fluidframework/aqueduct"; +import { IComponentHTMLView } from "@fluidframework/core-interfaces"; + +import * as React from "react"; +import * as ReactDOM from "react-dom"; +``` + +### Define our component class + +Below we define our component class `ExampleFluidComponent`. + +#### PrimedComponent + +Extending [PrimedComponent](../api/aqueduct.primedcomponent.md) sets up our component with required default +behavior as well as additional helpers to make component development easier. + +##### Key benefits + +1. Setup a `root` [SharedDirectory](../api/map.shareddirectory.md) (a Distributed Data Structure) that we can use to + store collaborative content and other distributed data structures. +2. Provide `this.createAndAttachComponent(...)` and `this.getComponent(...)` functions for easier creation and access + to other components. +3. Provide the following setup overrides + - `initializingFirstTime()` - only called the first time a component is initialized + - `existing()` - called every time except the first time a component is initialized + - `opened()` - called every time a component is initialized. After `create` and `existing`. + +#### IComponentHTMLView + +Implementing the [IComponentHTMLView](../api/core-interfaces.icomponenthtmlview.md) interface +denotes that our component can render an HTML view. Throughout the Fluid Framework we define interfaces as a way to +state our behavior. Whoever is attempting to use this component can know we support this interface and therefore it will +have a `render(...)` function. View rendering is explained more below. + +#### Code + +```typescript +export class ExampleFluidComponent extends PrimedComponent + implements IComponentHTMLView { + // ... +} +``` + +We also must implement our interface provider. As described above our component is viewable so it implements +`IComponentHTMLView`. By returning the component when this interface is queried, anyone who has a reference to +our component can discover that we implement `IComponentHTMLView`. + +```typescript +public get IComponentHTMLView() { return this; } +``` + +### `initializingFirstTime()` + +`initializingFirstTime()` will be called only the first time a client opens the component. In here we perform +setup operations that we only want to happen once. Since we are using a `PrimedComponent`, we have a `root` +SharedDirectory we can use to store data. We set our initial `diceValue` on our root directory like so: + +```typescript +protected async initializingFirstTime() { + this.root.set("diceValue", 1); +} +``` + +### `render(div: HTMLElement)` + +`render(div: HTMLElement)` is the implementation of `IComponentHTMLView`. The caller provides an `HTMLElement` that the +Component can use to render into. Every time `render(...)` is called we should produce a new view. + +::: note + +This is the point where React and VanillaJS differ. + +::: + +:::: tabs +::: tab React +We create a `rerender` function that will display our content into the provided `HTMLElement`. +To get the dice value we use the `get` method on the root, using the same key (`diceValue`) that +we created in `initializingFirstTime()`. Because we are using React we will call +`ReactDOM.render(...)` with a span displaying our dice value as a Unicode character and a button +that rolls the dice when clicked. Finally we pass the provided `HTMLElement` (`div`) into our +`ReactDOM.render(...)` to tell React what to render in. + +Once we've created our function we call it once to render the first time. + +```jsx +const rerender = () => { + // Get our dice value stored in the root. + const diceValue = this.root.get < number > "diceValue"; + + ReactDOM.render( +
+ {this.getDiceChar(diceValue)} + +
, + div + ); +}; + +rerender(); +``` + +Finally we add a listener so when the value of the dice changes we will trigger a render. + +```typescript +this.root.on("valueChanged", () => { + rerender(); +}); +``` + +::: +::: tab VanillaJS +The VanillaJS implementation is similar in many ways to the React version. + +We create our component's DOM structure in `this.createComponentDom(div);` which creates the span that +holds the dice value (`diceSpan.textContent = this.getDiceChar(diceValue);`) and the button that when clicked +rolls the dice (`rollButton.onclick = this.rollDice.bind(this);`). + +```typescript +private createComponentDom(host: HTMLElement) { + const diceValue = this.root.get("diceValue"); + + const diceSpan = document.createElement("span"); + diceSpan.id = "diceSpan"; + diceSpan.style.fontSize = "50px"; + diceSpan.textContent = this.getDiceChar(diceValue); + host.appendChild(diceSpan); + + const rollButton = document.createElement("button"); + rollButton.id = "rollButton"; + rollButton.textContent = "Roll"; + rollButton.onclick = this.rollDice.bind(this); + host.appendChild(rollButton); +} +``` + +And we register our function to re-render when the value of the dice changes. + +```typescript +this.root.on("valueChanged", () => { + const diceValue = this.root.get("diceValue"); + const diceSpan = document.getElementById("diceSpan"); + diceSpan.textContent = this.getDiceChar(diceValue); +}); +``` + +::: +:::: + +To set the value of the dice after rolling, we use the `set` method on the root, using the same +key `diceValue` as before. The helper functions used look like this: + +```typescript +private rollDice() { + const rollValue = Math.floor(Math.random() * 6) + 1; + this.root.set("diceValue", rollValue); +} + +private getDiceChar(value: number) { + // Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅) + return String.fromCodePoint(0x267F + value); +} +``` + +### Component Instantiation + +In order to make our component compatible with the Fluid Framework we must have a way of creating a +new instance. We require having an instantiation factory because it's required to define all supported +distributed data structures up front. Defining all the DDSs up front allows for the Fluid Framework to load +from a snapshot without worrying that something might exist in the snapshot that the framework can't understand. + +In the example below we use the [PrimedComponentFactory](../api/aqueduct.primedcomponentfactory.md) as a helper to +create our instantiation factory. As properties we pass in our supported distributed data structures. In this scenario +we don't use any additional distributed data structures, so we pass an empty array. + +```typescript +[], +``` + +The second property is an entry point into our component. + +```typescript +ExampleFluidComponent.load; +``` + +Finally we export this as `fluidExport`. This export is special - the `@fluidframework/webpack-component-loader` we +are using to load our component knows to look for this particular export to load from. + +```typescript +export const fluidExport = new PrimedComponentFactory( + ExampleFluidComponent, + [] +); +``` + +## Custom container + +If you instead chose to customize your container during the `yo fluid` setup, a couple things change. + +### Factory export + +Instead of exporting the `PrimedComponentFactory` directly as the `fluidExport`, we'll instead export this factory +for use in the container we're customizing (in [index.ts](#index-ts)). + +```typescript +export const ExampleFluidComponentInstantiationFactory = new PrimedComponentFactory( + ExampleFluidComponent, + [] +); +``` + +### `index.ts` + +You'll also have a file `./src/index.ts` for the container. In this file we define a registry of supported components. +This is represented as a `Map`. In our scenario we only have one component and therefore +one factory. + +We import our `ExampleFluidComponentInstantiationFactory` from our `./main`: + +```typescript +import { ExampleFluidComponentInstantiationFactory } from "./main"; +``` + +We import the `package.json` and use the package name as our component name. It's required when creating a new component +to provide this name. + +```typescript +const pkg = require("../package.json"); +const componentName = pkg.name as string; +``` + +Finally we use `ContainerRuntimeFactoryWithDefaultComponent` to create the `fluidExport`. The factory takes a default component +name `componentName` that is used to load the default component. It also takes the registry of components pointing to +the creation factory. In our case just our one component +(`[componentName, Promise.resolve(ExampleFluidComponentInstantiationFactory)]`). + +```typescript +export const fluidExport = new ContainerRuntimeFactoryWithDefaultComponent( + componentName, + new Map([ + [componentName, Promise.resolve(ExampleFluidComponentInstantiationFactory)] + ]) +); +``` diff --git a/docs/tutorials/dice-roller.ts b/docs/tutorials/dice-roller.ts new file mode 100644 index 000000000000..d9637f40ffbf --- /dev/null +++ b/docs/tutorials/dice-roller.ts @@ -0,0 +1,68 @@ +import { + PrimedComponent, + PrimedComponentFactory, +} from "@fluidframework/aqueduct"; +import { IFluidHTMLView } from "@fluidframework/view-interfaces"; + +const diceValueKey = "diceValue"; + +/** + * Fluid component + */ +export class HelloWorld extends PrimedComponent implements IFluidHTMLView { + public static get ComponentName() { + return "helloworld"; + } + + public get IFluidHTMLView() { return this; } + + /** + * The factory defines how to create an instance of the component as well as the + * dependencies of the component. + */ + public static readonly factory = new PrimedComponentFactory( + HelloWorld.ComponentName, + HelloWorld, + [], // Additional Distributed Data Structure Types + {}, // Providers (Advanced) + ); + + /** + * initializingFirstTime is called only once, it is executed only by the first client to open the + * component and all work will resolve before the view is presented to any user. + * + * This method is used to perform component setup, which can include setting an initial schema or initial values. + */ + protected async initializingFirstTime() { + this.root.set(diceValueKey, 1); + } + + /** + * Render the dice. + */ + public render(div: HTMLElement) { + const getDiceChar = (): string => { + return String.fromCodePoint(0x267F + this.root.get(diceValueKey)); + }; + const diceSpan = document.createElement("span"); + diceSpan.classList.add("diceSpan"); + diceSpan.style.fontSize = "50px"; + diceSpan.textContent = getDiceChar(); + div.appendChild(diceSpan); + + const rollButton = document.createElement("button"); + rollButton.classList.add("rollButton"); + rollButton.textContent = "Roll"; + rollButton.onclick = () => { + const rollValue = Math.floor(Math.random() * 6) + 1; + this.root.set(diceValueKey, rollValue); + }; + div.appendChild(rollButton); + + // When the value of the dice changes we will re-render the + // value in the dice span + this.root.on("valueChanged", () => { + diceSpan.textContent = getDiceChar(); + }); + } +} diff --git a/docs/tutorials/sudoku.md b/docs/tutorials/sudoku.md new file mode 100644 index 000000000000..88c2cb2231d9 --- /dev/null +++ b/docs/tutorials/sudoku.md @@ -0,0 +1,437 @@ +--- +title: "Sudoku" +uid: sudoku-example +sidebarDepth: 2 +--- + +::: danger + +OUTDATED + +::: + +In this example we will build a collaborative Sudoku game. We will use Fluid distributed data structures to store and +synchronize the Sudoku data. + +## Set up your dev environment + +If you haven't already, [set up your Fluid Framework development +environment](../guide/README.md#set-up-your-development-environment). + +### Clone the tutorial repository + +First, clone the tutorial repository here: . + +Once you've cloned the repo, you'll need to set up access to the [private Fluid npm feed](../guide/package-feed.md). On +Windows, you can run the `npm run auth` command to automate this process. + +Now that you have access to the private feed, run `npm install` in the root of the repository to install dependencies. + +Finally, you can open the folder in Visual Studio Code. + +## Acknowledgements + +This example uses the [sudokus](https://github.com/Moeriki/node-sudokus) npm package by Dieter Luypaert +() and the [@types/sudokus](https://www.npmjs.com/package/@types/sudokus) package by Florian +Keller (). + +### Folder layout + +The project has the following folder layout: + +``` +└───src + | fluidSudoku.tsx + │ index.ts + ├───helpers + │ coordinate.ts + │ puzzles.ts + | styles.css + │ sudokuCell.ts + └───react + sudokuView.tsx +``` + +The _src/fluid-components_ folder contains the source files for the Sudoku Fluid component. + +### Run the sample + +After you've cloned the sample repo and installed dependencies using `npm install`, you can then use `npm start` to start +a local dev environment for testing and debugging. Visit in a browser to load the Fluid +development server, which will load two instances of the component side by side. + +!!!include(browsers.md)!!! + +::: important + +If you make changes to your data model during development, you may notice console failures, or your component may fail +to load completely, when you refresh localhost:8080. This is caused when the local code tries to load a Fluid data model +that uses a schema different than what the code expects. You can force a fresh Fluid document, and by extension, an +empty schema, by reloading . This will redirect you to a new random Fluid document. + +::: + +## Deep dive + +### Data model + +For our Sudoku data model, we will use a map-like data structure with string keys. Each key in the map is a coordinate +(row, column) of a cell in the Sudoku puzzle. The top left cell has coordinate `"0,0"`, the cell to its right has +coordinate `"0,1"`, etc. + +Each value stored in the map is a `SudokuCell`, a simple class that contains the following properties: + +```typescript +value: number // The current value in the cell; 0 denotes an empty cell +isCorrect: boolean = false // True if the value in the cell is correct +readonly fixed: boolean; // True if the value in the cell is supplied as part of the puzzle's "clues" +readonly correctValue: number // Stores the correct value of the cell +readonly coordinate: CoordinateString // The coordinate of the cell, as a comma-separated string, e.g. "2,3" +``` + +::: important + +Objects that are stored in distributed data structures, as `SudokuCell` is, must be safely JSON-serializable. This means +that you cannot use functions or TypeScript class properties with these objects, because those are not JSON-serialized. + +One pattern to address this is to define static functions that accept the object as a parameter and manipulate it. See +the `SudokuCell` class in `/src/helpers/sudokuCell.ts` for an example of this pattern. + +::: + +### Rendering + +In order to render the Sudoku data, we use a React component called `SudokuView` This component is defined in +`src/react/sudokuView.tsx` and accepts the map of Sudoku cell data as a prop. It then renders the Sudoku and +accompanying UI. + +The `SudokuView` React component is also responsible for handling UI interaction from the user; we'll examine that in +more detail later. + +### The Fluid component + +The React component described above does not itself represent a Fluid component. Rather, the Fluid component is defined +in `src/fluidSudoku.tsx`. + +```typescript +export class FluidSudoku extends PrimedComponent implements IComponentHTMLView {} +``` + +This class extends the [PrimedComponent][] abstract base class. Our component is visual, so we need to implement the +[IComponentHTMLView][] or [IProvideComponentHTMLView][] interfaces. In our case, we want to handle rendering +ourselves rather than delegate it to another object, so we implement [IComponentHTMLView][]. + +#### Implementing interfaces + +##### IComponentHTMLView + +[IComponentHTMLView][] requires us to implement the `render()` method, which is straightforward since we're using the +`SudokuView` React component to do the heavy lifting. + +```typescript +public render(element?: HTMLElement): void { + if (element) { + this.domElement = element; + } + if (this.domElement) { + let view: JSX.Element; + if (this.puzzle) { + view = ( + + ); + } else { + view =
; + } + ReactDOM.render(view, this.domElement); + } +} +``` + +As you can see, the render method uses React to render the `SudokuView` React component. Notice that we pass the +puzzle data, a `SharedMap` distributed data structure that we will discuss more below, to the SudokuView React +component as props. + +#### Creating Fluid distributed data structures + +How does the `puzzle` property get populated? How are distributed data structures created and used? + +To answer that question, look at the `initializingFirstTime` method in the `FluidSudoku` class: + +```typescript +private sudokuMapKey = "sudoku-map"; +private puzzle: ISharedMap; + +protected async initializingFirstTime() { + // Create a new map for our Sudoku data + const map = SharedMap.create(this.runtime); + + // Populate it with some puzzle data + loadPuzzle(0, map); + + // Store the new map under the sudokuMapKey key in the root SharedDirectory + this.root.set(this.sudokuMapKey, map.handle); +} +``` + +This method is called once when a component is initially created. We create a new [SharedMap][] using `.create`, +registering it with the runtime. We have access to the Fluid runtime from `this.runtime` because we have subclassed +[PrimedComponent][]. + +Once the SharedMap is created, we populate it with puzzle data. Finally, we store the SharedMap we just created in the +`root` [SharedDirectory][]. The `root` [SharedDirectory][] is provided by [PrimedComponent][], and is a convenient place +to store all Fluid data used by your component. + +Notice that we provide a string key, `this.sudokuMapKey`, when we store the `SharedMap`. This is how we will retrieve +the data structure from the root SharedDirectory later. + +`initializingFirstTime` is only called the _first time_ the component is created. This is exactly what we want +in order to create the distributed data structures. We don't want to create new SharedMaps every time a client loads the +component! However, we do need to _load_ the distributed data structures each time the component is loaded. + +Distributed data structures are initialized asynchronously, so we need to retrieve them from within an asynchronous +method. We do that by overloading the `hasInitialized` method, then store a local reference to the object +(`this.puzzle`) so we can easily use it in synchronous code. + +```typescript +protected async hasInitialized() { + this.puzzle = await this.root.get(this.sudokuMapKey).get(); +} +``` + +The `hasInitialized` method is called once after the component has completed initialization, be it the first +time or subsequent times. + +##### A note about component handles + +You probably noticed some confusing code above. What are handles? Why do we store the SharedMap's _handle_ in the `root` +SharedDirectory instead of the SharedMap itself? The underlying reasons are beyond the scope of this example, but the +important thing to remember is this: + +**When you store a distributed data structure within another distributed data structure, you store the _handle_ to the +DDS, not the DDS itself. Similarly, when loading a DDS that is stored within another DDS, you must first get the DDS +handle, then get the full DDS from the handle.** + +```typescript +await this.root.get(this.sudokuMapKey).get(); +``` + +#### Handling events from distributed data structures + +Distributed data structures can be changed by both local code and remote clients. In the `hasInitialized` +method, we also connect a method to be called each time the Sudoku data - the [SharedMap][] - is changed. In our case we +simply call `render` again. This ensures that our UI updates whenever a remote client changes the Sudoku data. + +```typescript +this.puzzle.on("valueChanged", (changed, local, op) => { + this.render(); +}); +``` + +#### Updating distributed data structures + +In the previous step we showed how to use event listeners with distributed data structures to respond to remote data +changes. But how do we update the data based on _user_ input? To do that, we need to listen to some DOM events as users +enter data in the Sudoku cells. Since the `SudokuView` class handles the rendering, that's where the DOM events will be +handled. + +Let's look at the `numericInput` function, which is called when the user keys in a number. + +::: note + +The `numericInput` function can be found in the `SimpleTable` React component within `src/react/sudokuView.tsx`. +`SimpleTable` is a helper React component that is not exported; you can consider it part of the `SudokuView` React +component. + +::: + +```typescript{2-6,9} +const numericInput = (keyString: string, coord: string) => { + let valueToSet = Number(keyString); + valueToSet = Number.isNaN(valueToSet) ? 0 : valueToSet; + if (valueToSet >= 10 || valueToSet < 0) { + return; + } + + if (coord !== undefined) { + const cellInputElement = getCellInputElement(coord); + cellInputElement.value = keyString; + + const toSet = props.puzzle.get(coord); + if (toSet.fixed) { + return; + } + toSet.value = valueToSet; + toSet.isCorrect = valueToSet === toSet.correctValue; + props.puzzle.set(coord, toSet); + } +}; +``` + +Lines 2-6 ensure we only accept single-digit numeric values. In line 9, we retrieve the coordinate of the cell from a DOM +attribute that we added during render. Once we have the coordinate, which is a key in the `SharedMap` storing our Sudoku +data, we retrieve the cell data by calling `.get(coord)`. We then update the cell's value and set whether it +is correct. Finally, we call `.set(key, toSet)` to update the data in the `SharedMap`. + +This pattern of first retrieving an object from a `SharedMap`, updating it, then setting it again, is an idiomatic Fluid +pattern. Without calling `.set()`, other clients will not be notified of the updates to the values within the map. By +setting the value, we ensure that Fluid notifies all other clients of the change. + +Once the value is set, the `valueChanged` event will be raised on the SharedMap, and as you'll recall from the previous +section, we listen to that event and render again every time the values change. Both local and remote clients will +render based on this event, because all clients are running the same code. + +**This is an important design principle:** components should have the same logic for handling local and remote changes. +In other words, it is very rare that there is a need for the handling to differ, and we recommend a unidirectional data +flow. + +## Lab: Adding "presence" to the Fluid Sudoku component + +The Sudoku component is collaborative; multiple clients can update the cells in real time. However, there's no +indication of where other clients are - which cells they're in. In this lab we'll add basic 'presence' to our Sudoku +component, so we can see where other clients are. + +To do this, we'll create a new `SharedMap` to store the presence information. Like the map we're using for Sudoku data, +it will be a map of cell coordinates to client names. As clients select cells, the presence map will be updated with the +current client in the cell. + +Note that using a SharedMap for presence means that the history of each user's movement - their presence - will be +persisted in the Fluid op stream. In the Sudoku scenario, maintaining a history of a client's movement isn't +particularly interesting, and Fluid provides an alternative mechanism, _signals_, to address cases where persisting ops +isn't necessary. That said, this serves as a useful example of how to use Fluid to solve complex problems with very +little code. + +### Create a SharedMap to contain presence data + +First, you need to create a `SharedMap` for your presence data. + +1. Open `src/fluidSudoku.tsx`. +1. Inside the `FluidSudoku` class, declare two new private variables like so: + + ```ts + private readonly presenceMapKey = "clientPresence"; + private clientPresence: ISharedMap | undefined; + ``` + +1. Inside the `initializingFirstTime` method, add the following code to the bottom of the method to create and + register a second `SharedMap`: + + ```ts + // Create a SharedMap to store presence data + const clientPresence = SharedMap.create(this.runtime); + this.root.set(this.presenceMapKey, clientPresence.handle); + ``` + + Notice that the Fluid runtime is exposed via the `this.runtime` property provided by [PrimedComponent][]. + +1. Inside the `hasInitialized` method, add the following code to the bottom of the method to retrieve the + presence map when the component initializes: + + ```ts + this.clientPresence = await this.root + .get(this.presenceMapKey) + .get(); + ``` + +You now have a `SharedMap` to store presence data. When the component is first created, `initializingFirstTime` +will be called and the presence map will be created. When the component is loaded, `hasInitialized` will be +called, which retrieves the `SharedMap` instance. + +### Rendering presence + +Now that you have a presence map, you need to render some indication that a remote user is in a cell. We're going to +take a shortcut here because our SudokuView React component can already display presence information when provided two +optional props: + +```ts +clientPresence?: ISharedMap; +setPresence?(cellCoord: CoordinateString, reset: boolean): void; +``` + +We aren't providing those props, so the presence display capabilities within the React component aren't enabled. After +you've completed this tutorial, you should consider reviewing the implementation of the presence rendering within +SudokuView in detail. For now, however, we'll skip that and focus on implementing the two necessary props - a SharedMap +for storing the presence data, and a function to update the map with presence data. + +### Setting presence data + +1. Open `src/fluidSudoku.tsx`. +1. Add the following function at the bottom of the `FluidSudoku` class: + + ```ts + /** + * A function that can be used to update presence data. + * + * @param cellCoordinate - The coordinate of the cell to set. + * @param reset - If true, presence for the cell will be cleared. + */ + private readonly presenceSetter = (cellCoordinate: string, reset: boolean): void => { + if (this.clientPresence) { + if (reset) { + // Retrieve the current clientId in the cell, if there is one + const prev = this.clientPresence.get(cellCoordinate); + const isCurrentClient = this.runtime.clientId === prev; + if (!isCurrentClient) { + return; + } + this.clientPresence.delete(cellCoordinate); + } else { + this.clientPresence.set(cellCoordinate, this.runtime.clientId); + } + } + }; + ``` + + You can pass this function in to the `SudokuView` React component as a prop. The React component will call + `presenceSetter` when users enter and leave cells, which will update the presence `SharedMap`. + +1. Replace the `createJSXElement` method with the following code: + + ```ts + public createJSXElement(): JSX.Element { + if (this.puzzle) { + return ( + + ); + } else { + return
; + } + } + ``` + + Notice that we're now passing the `clientPresence` SharedMap and the `setPresence` function as props. + +### Listening to distributed data structure events + +1. Still in `src/fluidSudoku.tsx`, add the following code to the bottom of the `hasInitialized` method to call + render whenever a remote change is made to the presence map: + + ```ts + this.clientPresence.on("valueChanged", (changed, local, op) => { + this.render(); + }); + ``` + +### Testing the changes + +Now run `npm start` again and notice that your selected cell is now highlighted on the other side. + +## Next steps + +Now that you have some experience with Fluid, are there other features you could add to the Sudoku component? Perhaps +you could extend it to display a client name in the cell to show client-specific presence. Or you could use the +[undo-redo][] package to add undo/redo support! + +Or check out [other tutorials](./README.md). + + +!!!include(links.md)!!! diff --git a/examples/data-objects/client-ui-lib/package.json b/examples/data-objects/client-ui-lib/package.json index 8f87cb0ad3d5..349e202643c1 100644 --- a/examples/data-objects/client-ui-lib/package.json +++ b/examples/data-objects/client-ui-lib/package.json @@ -58,6 +58,7 @@ "@fluidframework/map": "^0.25.0", "@fluidframework/merge-tree": "^0.25.0", "@fluidframework/protocol-definitions": "^0.1011.0-0", + "@fluidframework/request-handler": "^0.25.0", "@fluidframework/runtime-definitions": "^0.25.0", "@fluidframework/runtime-utils": "^0.25.0", "@fluidframework/sequence": "^0.25.0", diff --git a/examples/data-objects/client-ui-lib/src/controls/flowView.ts b/examples/data-objects/client-ui-lib/src/controls/flowView.ts index 5ea024f28492..54f62ca3f2e9 100644 --- a/examples/data-objects/client-ui-lib/src/controls/flowView.ts +++ b/examples/data-objects/client-ui-lib/src/controls/flowView.ts @@ -24,6 +24,7 @@ import { SharedSegmentSequenceUndoRedoHandler, UndoRedoStackManager } from "@flu import { HTMLViewAdapter } from "@fluidframework/view-adapters"; import { IFluidHTMLView } from "@fluidframework/view-interfaces"; import { requestFluidObject } from "@fluidframework/runtime-utils"; +import { handleFromLegacyUri } from "@fluidframework/request-handler"; import { blobUploadHandler } from "../blob"; import { CharacterCodes, Paragraph, Table } from "../text"; import * as ui from "../ui"; @@ -815,16 +816,17 @@ function renderSegmentIntoLine( if (componentMarker.instance === undefined) { if (componentMarker.instanceP === undefined) { // eslint-disable-next-line @typescript-eslint/no-floating-promises - requestFluidObject( - lineContext.flowView.collabDocument.context.containerRuntime.IFluidHandleContext, - `/${componentMarker.properties.leafId}`) - .then(async (component) => { - if (!HTMLViewAdapter.canAdapt(component)) { - return Promise.reject("component is not viewable"); - } + handleFromLegacyUri( + `/${componentMarker.properties.leafId}`, + lineContext.flowView.collabDocument.context.containerRuntime) + .get() + .then(async (component) => { + if (!HTMLViewAdapter.canAdapt(component)) { + return Promise.reject("component is not viewable"); + } - return new HTMLViewAdapter(component); - }); + return new HTMLViewAdapter(component); + }); // eslint-disable-next-line @typescript-eslint/no-floating-promises componentMarker.instanceP.then((instance) => { diff --git a/examples/data-objects/multiview/container/src/container.tsx b/examples/data-objects/multiview/container/src/container.tsx index f71c0706f2b7..476f1059aa1b 100644 --- a/examples/data-objects/multiview/container/src/container.tsx +++ b/examples/data-objects/multiview/container/src/container.tsx @@ -38,13 +38,15 @@ const createAndAttachCoordinate = async (runtime: IContainerRuntime, id: string) }; // Just a little helper, since we're going to request multiple coordinates. -const requestCoordinateFromId = async (request: RequestParser, runtime: IContainerRuntime, id: string) => { +async function requestObjectStoreFromId(request: RequestParser, runtime: IContainerRuntime, id: string) { const coordinateRequest = new RequestParser({ - url: `${id}`, + url: ``, headers: request.headers, }); - return requestFluidObject(runtime.IFluidHandleContext, coordinateRequest); -}; + return requestFluidObject( + await runtime.getRootDataStore(id), + coordinateRequest); +} /** * When someone requests the default view off our container ("/"), we'll respond with a DefaultView. To do so, @@ -53,17 +55,16 @@ const requestCoordinateFromId = async (request: RequestParser, runtime: IContain const defaultViewRequestHandler: RuntimeRequestHandler = async (request: RequestParser, runtime: IContainerRuntime) => { if (request.pathParts.length === 0) { - const simpleCoordinate = await requestCoordinateFromId(request, runtime, simpleCoordinateComponentId); - const triangleCoordinate1 = await requestCoordinateFromId(request, runtime, triangleCoordinateComponentId1); - const triangleCoordinate2 = await requestCoordinateFromId(request, runtime, triangleCoordinateComponentId2); - const triangleCoordinate3 = await requestCoordinateFromId(request, runtime, triangleCoordinateComponentId3); - const constellationRequest = new RequestParser({ - url: `${constellationComponentId}`, - headers: request.headers, - }); - const constellation = await requestFluidObject( - runtime.IFluidHandleContext, - constellationRequest); + const simpleCoordinate = await requestObjectStoreFromId( + request, runtime, simpleCoordinateComponentId); + const triangleCoordinate1 = await requestObjectStoreFromId( + request, runtime, triangleCoordinateComponentId1); + const triangleCoordinate2 = await requestObjectStoreFromId( + request, runtime, triangleCoordinateComponentId2); + const triangleCoordinate3 = await requestObjectStoreFromId( + request, runtime, triangleCoordinateComponentId3); + const constellation = await requestObjectStoreFromId( + request, runtime, constellationComponentId); const viewResponse = ( ( - this.context.containerRuntime.IFluidHandleContext, - `/${SchedulerType}`); - this.taskManager = schedulerComponent.ITaskManager; + this.taskManager = await this.context.containerRuntime.getTaskManager(); const options = parse(window.location.search.substr(1)); setTranslation( diff --git a/examples/data-objects/vltava/package.json b/examples/data-objects/vltava/package.json index f6b8fbcee5ad..d82533a09680 100644 --- a/examples/data-objects/vltava/package.json +++ b/examples/data-objects/vltava/package.json @@ -47,6 +47,7 @@ "@fluidframework/last-edited-experimental": "^0.25.0", "@fluidframework/map": "^0.25.0", "@fluidframework/protocol-definitions": "^0.1011.0-0", + "@fluidframework/request-handler": "^0.25.0", "@fluidframework/runtime-definitions": "^0.25.0", "@fluidframework/runtime-utils": "^0.25.0", "@fluidframework/view-adapters": "^0.25.0", diff --git a/examples/data-objects/vltava/src/components/anchor/anchor.ts b/examples/data-objects/vltava/src/components/anchor/anchor.ts index f9e61634dda0..4f03f23ca1ca 100644 --- a/examples/data-objects/vltava/src/components/anchor/anchor.ts +++ b/examples/data-objects/vltava/src/components/anchor/anchor.ts @@ -8,11 +8,10 @@ import { DataObject, DataObjectFactory } from "@fluidframework/aqueduct"; import { IFluidLastEditedTracker, IProvideFluidLastEditedTracker, - LastEditedTrackerDataObjectName, + LastEditedTrackerDataObject, } from "@fluidframework/last-edited-experimental"; import { IFluidHTMLView, IProvideFluidHTMLView } from "@fluidframework/view-interfaces"; - -export const AnchorName = "anchor"; +import { Vltava } from "../vltava"; /** * Anchor is an default component is responsible for managing creation and the default component @@ -31,7 +30,7 @@ export class Anchor extends DataObject implements IProvideFluidHTMLView, IProvid return this.defaultComponentInternal; } - private static readonly factory = new DataObjectFactory(AnchorName, Anchor, [], {}); + private static readonly factory = new DataObjectFactory("anchor", Anchor, [], {}); public static getFactory() { return Anchor.factory; @@ -48,10 +47,10 @@ export class Anchor extends DataObject implements IProvideFluidHTMLView, IProvid } protected async initializingFirstTime() { - const defaultComponent = await this.createFluidObject("vltava"); + const defaultComponent = await Vltava.getFactory().createInstance(this.context); this.root.set(this.defaultComponentId, defaultComponent.handle); - const lastEditedComponent = await this.createFluidObject(LastEditedTrackerDataObjectName); + const lastEditedComponent = await LastEditedTrackerDataObject.getFactory().createInstance(this.context); this.root.set(this.lastEditedComponentId, lastEditedComponent.handle); } diff --git a/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx b/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx index e40717b1ff17..a0ef792cd769 100644 --- a/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx +++ b/examples/data-objects/vltava/src/components/tabs/newTabButton.tsx @@ -4,6 +4,7 @@ */ import React from "react"; +import { fluidExport as pmfe } from "@fluid-example/prosemirror/dist/prosemirror"; import { IButtonStyles, IconButton, @@ -66,7 +67,7 @@ export const NewTabButton: React.FunctionComponent = styles={customSplitButtonStyles} menuProps={menuProps} ariaLabel="New item" - onClick={() => props.createTab("prosemirror")} // this should be taken from the list + onClick={() => props.createTab(pmfe.type)} // this should be taken from the list disabled={disabled} checked={checked} text="hello" diff --git a/examples/data-objects/vltava/src/components/vltava/dataModel.ts b/examples/data-objects/vltava/src/components/vltava/dataModel.ts index 734834315121..4758450e11c9 100644 --- a/examples/data-objects/vltava/src/components/vltava/dataModel.ts +++ b/examples/data-objects/vltava/src/components/vltava/dataModel.ts @@ -11,7 +11,8 @@ import { IFluidDataStoreContext } from "@fluidframework/runtime-definitions"; import { IFluidDataStoreRuntime } from "@fluidframework/datastore-definitions"; import { ISharedDirectory } from "@fluidframework/map"; import { IQuorum, ISequencedClient } from "@fluidframework/protocol-definitions"; -import { requestFluidObject } from "@fluidframework/runtime-utils"; +import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqueduct"; +import { handleFromLegacyUri } from "@fluidframework/request-handler"; export interface IVltavaUserDetails { name: string, @@ -118,7 +119,9 @@ export class VltavaDataModel extends EventEmitter implements IVltavaDataModel { } private async setupLastEditedTracker() { - const object = await requestFluidObject(this.context.containerRuntime.IFluidHandleContext, "default"); - this.lastEditedTracker = object.IFluidLastEditedTracker; + const handle = handleFromLegacyUri( + ContainerRuntimeFactoryWithDefaultDataStore.defaultComponentId, + this.context.containerRuntime); + this.lastEditedTracker = (await handle.get()).IFluidLastEditedTracker; } } diff --git a/examples/data-objects/vltava/src/components/vltava/vltava.tsx b/examples/data-objects/vltava/src/components/vltava/vltava.tsx index 1cf401df94d7..87a33d1989cd 100644 --- a/examples/data-objects/vltava/src/components/vltava/vltava.tsx +++ b/examples/data-objects/vltava/src/components/vltava/vltava.tsx @@ -12,15 +12,13 @@ import ReactDOM from "react-dom"; import { IVltavaDataModel, VltavaDataModel } from "./dataModel"; import { VltavaView } from "./view"; -export const VltavaName = "vltava"; - /** * Vltava is an application experience */ export class Vltava extends DataObject implements IFluidHTMLView { private dataModelInternal: IVltavaDataModel | undefined; - private static readonly factory = new DataObjectFactory(VltavaName, Vltava, [], {}); + private static readonly factory = new DataObjectFactory("vltava", Vltava, [], {}); public static getFactory() { return Vltava.factory; diff --git a/examples/data-objects/vltava/src/containerServices/match-maker/matchMaker.ts b/examples/data-objects/vltava/src/containerServices/match-maker/matchMaker.ts index 77e96094db0d..45102ee275d0 100644 --- a/examples/data-objects/vltava/src/containerServices/match-maker/matchMaker.ts +++ b/examples/data-objects/vltava/src/containerServices/match-maker/matchMaker.ts @@ -14,16 +14,16 @@ import { IComponentDiscoverableInterfaces, } from "@fluidframework/framework-interfaces"; import { IFluidDataStoreContext } from "@fluidframework/runtime-definitions"; -import { requestFluidObject } from "@fluidframework/runtime-utils"; +import { handleFromLegacyUri } from "@fluidframework/request-handler"; export const MatchMakerContainerServiceId = "matchMaker"; const getMatchMakerContainerService = async (context: IFluidDataStoreContext): Promise => { - const value = await requestFluidObject( - context.containerRuntime.IFluidHandleContext, - `/${serviceRoutePathRoot}/${MatchMakerContainerServiceId}`); - const matchMaker = value.IComponentInterfacesRegistry; + const handle = handleFromLegacyUri( + `/${serviceRoutePathRoot}/${MatchMakerContainerServiceId}`, + context.containerRuntime); + const matchMaker = (await handle.get()).IComponentInterfacesRegistry; if (matchMaker) { return matchMaker; } diff --git a/examples/data-objects/vltava/src/index.ts b/examples/data-objects/vltava/src/index.ts index c46a5103ab8a..31b8463bd957 100644 --- a/examples/data-objects/vltava/src/index.ts +++ b/examples/data-objects/vltava/src/index.ts @@ -11,22 +11,21 @@ import { ContainerRuntimeFactoryWithDefaultDataStore } from "@fluidframework/aqu import { IFluidObject } from "@fluidframework/core-interfaces"; import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; import { - LastEditedTrackerDataObjectName, LastEditedTrackerDataObject, setupLastEditedTrackerForContainer, + IFluidLastEditedTracker, } from "@fluidframework/last-edited-experimental"; import { IFluidDataStoreRegistry, IProvideFluidDataStoreFactory, NamedFluidDataStoreRegistryEntries, } from "@fluidframework/runtime-definitions"; +import { requestFluidObject } from "@fluidframework/runtime-utils"; import { Anchor, - AnchorName, TabsComponent, Vltava, - VltavaName, } from "./components"; import { IComponentInternalRegistry, @@ -80,49 +79,46 @@ export class VltavaRuntimeFactory extends ContainerRuntimeFactoryWithDefaultData protected async containerHasInitialized(runtime: IContainerRuntime) { // Load the last edited tracker component (done by the setup method below). This component provides container // level tracking of last edit and has to be loaded before any other component. + const tracker = await requestFluidObject( + await runtime.getRootDataStore(ContainerRuntimeFactoryWithDefaultDataStore.defaultComponentId), + ""); - // Right now this setup has to be done asynchronously because in the case where we load the Container from - // remote ops, the `Attach` message for the last edited tracker component has not arrived yet. - // We should be able to wait here after the create-new workflow is in place. - setupLastEditedTrackerForContainer(ContainerRuntimeFactoryWithDefaultDataStore.defaultComponentId, runtime) - .catch((error) => { - console.error(error); - }); + setupLastEditedTrackerForContainer(tracker.IFluidLastEditedTracker, runtime); } } const generateFactory = () => { const containerComponentsDefinition: IInternalRegistryEntry[] = [ { - type: "clicker", + type: Anchor.getFactory().type, factory: Promise.resolve(ClickerInstantiationFactory), capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Clicker", fabricIconName: "NumberField", }, { - type: "tabs", + type: TabsComponent.getFactory().type, factory: Promise.resolve(TabsComponent.getFactory()), capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Tabs", fabricIconName: "BrowserTab", }, { - type: "spaces", + type: Spaces.getFactory().type, factory: Promise.resolve(Spaces.getFactory()), capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Spaces", fabricIconName: "SnapToGrid", }, { - type: "codemirror", + type: cmfe.type, factory: Promise.resolve(cmfe), capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Codemirror", fabricIconName: "Code", }, { - type: "prosemirror", + type: pmfe.type, factory: Promise.resolve(pmfe), capabilities: ["IFluidHTMLView", "IFluidLoadable"], friendlyName: "Prosemirror", @@ -135,26 +131,17 @@ const generateFactory = () => { containerComponents.push([value.type, value.factory]); }); - // The last edited tracker component provides container level tracking of last edits. This is the first - // component that is loaded. - containerComponents.push( - [LastEditedTrackerDataObjectName, Promise.resolve(LastEditedTrackerDataObject.getFactory())]); - - // We don't want to include the default wrapper component in our list of available components - containerComponents.push([AnchorName, Promise.resolve(Anchor.getFactory())]); - containerComponents.push([VltavaName, Promise.resolve(Vltava.getFactory())]); - - const containerRegistries: NamedFluidDataStoreRegistryEntries = [ - ["", Promise.resolve(new InternalRegistry(containerComponentsDefinition))], - ]; - // TODO: You should be able to specify the default registry instead of just a list of components // and the default registry is already determined Issue:#1138 return new VltavaRuntimeFactory( - AnchorName, + Anchor.getFactory().type, [ ...containerComponents, - ...containerRegistries, + LastEditedTrackerDataObject.getFactory().registryEntry, + // We don't want to include the default wrapper component in our list of available components + Anchor.getFactory().registryEntry, + Vltava.getFactory().registryEntry, + ["", Promise.resolve(new InternalRegistry(containerComponentsDefinition))], ], ); }; diff --git a/packages/framework/aqueduct/README.md b/packages/framework/aqueduct/README.md index bbbb733666f5..09a9ccecc93a 100644 --- a/packages/framework/aqueduct/README.md +++ b/packages/framework/aqueduct/README.md @@ -28,9 +28,9 @@ The [`SharedComponent`](./src/components/sharedComponent.ts) provides the follow - Basic set of interface implementations to be loadable in a Fluid Container. - Functions for managing component lifecycle. - - `componentInitializingFirstTime(props: S)` - called only the first time a component is initialized - - `componentInitializingFromExisting()` - called every time except the first time a component is initialized - - `componentHasInitialized()` - called every time after `componentInitializingFirstTime` or `componentInitializingFromExisting` executes + - `initializingFirstTime(props: S)` - called only the first time a component is initialized + - `initializingFromExisting()` - called every time except the first time a component is initialized + - `hasInitialized()` - called every time after `initializingFirstTime` or `initializingFromExisting` executes - Helper functions for creating and getting other Component Objects in the same Container. > Note: You probably don't want to inherit from this component directly unless you are creating another base component class. If you have a component that doesn't use Distributed Data Structures you should use Container Services to manage your object. @@ -47,12 +47,12 @@ export class Clicker extends PrimedComponent implements IComponentHTMLView { private _counter: SharedCounter | undefined; - protected async componentInitializingFirstTime() { + protected async initializingFirstTime() { const counter = SharedCounter.create(this.runtime); this.root.set("clicks", counter.handle); } - protected async componentHasInitialized() { + protected async hasInitialized() { const counterHandle = this.root.get>("clicks"); this._counter = await counterHandle.get(); } @@ -120,7 +120,7 @@ On our example we want to declare that we want the `IComponentUserInfo` Provider ```typescript export class MyExample extends PrimedComponent { - protected async componentInitializingFirstTime() { + protected async initializingFirstTime() { const userInfo = await this.providers.IComponentUserInfo; if(userInfo) { console.log(userInfo.userCount); diff --git a/packages/framework/aqueduct/src/data-objects/blobHandle.ts b/packages/framework/aqueduct/src/data-objects/blobHandle.ts index 33afc3fb590f..c247775ee45e 100644 --- a/packages/framework/aqueduct/src/data-objects/blobHandle.ts +++ b/packages/framework/aqueduct/src/data-objects/blobHandle.ts @@ -22,7 +22,6 @@ import { ISharedDirectory } from "@fluidframework/map"; */ export class BlobHandle implements IFluidHandle { public get IFluidRouter(): IFluidRouter { return this; } - public get IFluidHandleContext(): IFluidHandleContext { return this; } public get IFluidHandle(): IFluidHandle { return this; } public get isAttached(): boolean { @@ -32,7 +31,7 @@ export class BlobHandle implements IFluidHandle { public readonly absolutePath: string; constructor( - public readonly path: string, + private readonly path: string, private readonly directory: ISharedDirectory, public readonly routeContext: IFluidHandleContext, ) { diff --git a/packages/framework/aqueduct/src/data-objects/dataObject.ts b/packages/framework/aqueduct/src/data-objects/dataObject.ts index 7fc7742e1028..6cdfe5288fc6 100644 --- a/packages/framework/aqueduct/src/data-objects/dataObject.ts +++ b/packages/framework/aqueduct/src/data-objects/dataObject.ts @@ -10,10 +10,9 @@ import { IResponse, } from "@fluidframework/core-interfaces"; import { ISharedDirectory, MapFactory, SharedDirectory } from "@fluidframework/map"; -import { ITaskManager, SchedulerType } from "@fluidframework/runtime-definitions"; +import { ITaskManager } from "@fluidframework/runtime-definitions"; import { v4 as uuid } from "uuid"; import { IEvent } from "@fluidframework/common-definitions"; -import { requestFluidObject } from "@fluidframework/runtime-utils"; import { BlobHandle } from "./blobHandle"; import { PureDataObject } from "./pureDataObject"; @@ -96,9 +95,7 @@ export abstract class DataObject

{ // Initialize task manager. - this.internalTaskManager = await requestFluidObject( - this.context.containerRuntime.IFluidHandleContext, - `/${SchedulerType}`); + this.internalTaskManager = await this.context.containerRuntime.getTaskManager(); if (!this.runtime.existing) { // Create a root directory and register it before calling initializingFirstTime diff --git a/packages/framework/aqueduct/src/data-objects/pureDataObject.ts b/packages/framework/aqueduct/src/data-objects/pureDataObject.ts index 467228312370..0b2015d59b41 100644 --- a/packages/framework/aqueduct/src/data-objects/pureDataObject.ts +++ b/packages/framework/aqueduct/src/data-objects/pureDataObject.ts @@ -20,6 +20,7 @@ import { IDirectory } from "@fluidframework/map"; import { EventForwarder } from "@fluidframework/common-utils"; import { IEvent } from "@fluidframework/common-definitions"; import { requestFluidObject } from "@fluidframework/runtime-utils"; +import { handleFromLegacyUri } from "@fluidframework/request-handler"; import { serviceRoutePathRoot } from "../container-services"; export interface ISharedComponentProps

{ @@ -206,7 +207,7 @@ export abstract class PureDataObject

(id: string): Promise { - return requestFluidObject(this.context.containerRuntime.IFluidHandleContext, `/${id}`); + return handleFromLegacyUri(`/${id}`, this.context.containerRuntime).get(); } /** @@ -214,7 +215,7 @@ export abstract class PureDataObject

(id: string): Promise { - return requestFluidObject(this.context.containerRuntime.IFluidHandleContext, `/${serviceRoutePathRoot}/${id}`); + return handleFromLegacyUri(`/${serviceRoutePathRoot}/${id}`, this.context.containerRuntime).get(); } /** diff --git a/packages/framework/aqueduct/src/request-handlers/requestHandlers.ts b/packages/framework/aqueduct/src/request-handlers/requestHandlers.ts index 9a06ce10e3d8..5e0b2eb15168 100644 --- a/packages/framework/aqueduct/src/request-handlers/requestHandlers.ts +++ b/packages/framework/aqueduct/src/request-handlers/requestHandlers.ts @@ -58,7 +58,9 @@ export const defaultRouteRequestHandler = (defaultRootId: string) => { return async (request: IRequest, runtime: IContainerRuntime) => { const parser = new RequestParser(request); if (parser.pathParts.length === 0) { - return runtime.resolveHandle({ url: `/${defaultRootId}${parser.query}`, headers: request.headers }); + return runtime.IFluidHandleContext.resolveHandle({ + url: `/${defaultRootId}${parser.query}`, + headers: request.headers }); } return undefined; // continue search }; diff --git a/packages/framework/aqueduct/src/test/defaultRoute.spec.ts b/packages/framework/aqueduct/src/test/defaultRoute.spec.ts index 0fb7e5852dc3..b1bf084e026f 100644 --- a/packages/framework/aqueduct/src/test/defaultRoute.spec.ts +++ b/packages/framework/aqueduct/src/test/defaultRoute.spec.ts @@ -8,11 +8,18 @@ import assert from "assert"; import { RequestParser } from "@fluidframework/runtime-utils"; import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; import { IFluidDataStoreChannel } from "@fluidframework/runtime-definitions"; -import { IRequest, IResponse, IFluidObject, IFluidRouter } from "@fluidframework/core-interfaces"; +import { + IRequest, + IResponse, + IFluidObject, + IFluidRouter, +} from "@fluidframework/core-interfaces"; import { createComponentResponse } from "@fluidframework/request-handler"; import { defaultRouteRequestHandler } from "../request-handlers"; class MockRuntime { + public get IFluidHandleContext() { return this; } + public async getRootDataStore(id, wait): Promise { if (id === "componentId") { return { @@ -29,7 +36,7 @@ class MockRuntime { throw new Error("No component"); } - public async resolveHandle(request: IRequest) { + protected async resolveHandle(request: IRequest) { const requestParser = new RequestParser(request); if (requestParser.pathParts.length > 0) { @@ -60,7 +67,7 @@ async function assertRejected(p: Promise) { } describe("defaultRouteRequestHandler", () => { - const runtime = new MockRuntime() as IContainerRuntime; + const runtime = new MockRuntime() as any as IContainerRuntime; it("Component request with default ID", async () => { const handler = defaultRouteRequestHandler("componentId"); diff --git a/packages/framework/last-edited-experimental/README.md b/packages/framework/last-edited-experimental/README.md index fe649252eda1..52a4bd01fc7a 100644 --- a/packages/framework/last-edited-experimental/README.md +++ b/packages/framework/last-edited-experimental/README.md @@ -32,19 +32,8 @@ It implements IProvideComponentLastEditedTracker and returns an IComponentLastEd # Setup This package also provides a `setupLastEditedTrackerForContainer` method that can be used to set up a component that provides IComponentLastEditedTracker to track last edited in a Container: -``` -async function setupLastEditedTrackerForContainer( - componentId: string, - runtime: IContainerRuntime, - shouldDiscardMessageFn: (message: ISequencedDocumentMessage) => boolean = shouldDiscardMessageDefault, -) -``` - -- The component with id "componentId" must implement an IComponentLastEditedTracker. - This setup function should be called during container instantiation so that ops are not missed. -- Requests the root component from the runtime and waits for it to load. - Registers an "op" listener on the runtime. On each message, it calls the shouldDiscardMessageFn to check if the message should be discarded. It also discards all scheduler message. If a message is not discarded, it passes the last edited information from the message to the last edited tracker in the component. -- The last edited information from the last message received before the component is loaded is stored and passed to the tracker once the component loads. Note: - By default, message that are not of `"Attach"` and `"Operation"` type are discarded as per the `shouldDiscardMessageDefault` function: @@ -75,10 +64,7 @@ public async instantiateRuntime(context: IContainerContext): Promise { await runtime.createComponent(componentId, "lastEditedTracker"); } - setupLastEditedTrackerForContainer(componentId, runtime) - .catch((error) => { - throw error; - }); + setupLastEditedTrackerForContainer(componentId, runtime); return runtime; } diff --git a/packages/framework/last-edited-experimental/src/lastEditedTrackerComponent.ts b/packages/framework/last-edited-experimental/src/lastEditedTrackerComponent.ts index cb39ca07c53e..cfeb9c6becbf 100644 --- a/packages/framework/last-edited-experimental/src/lastEditedTrackerComponent.ts +++ b/packages/framework/last-edited-experimental/src/lastEditedTrackerComponent.ts @@ -9,17 +9,13 @@ import { IFluidHandle } from "@fluidframework/core-interfaces"; import { LastEditedTracker } from "./lastEditedTracker"; import { IProvideFluidLastEditedTracker } from "./interfaces"; -// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -const pkg = require("../package.json"); -export const LastEditedTrackerDataObjectName = pkg.name as string; - /** * LastEditedTrackerDataObject creates a LastEditedTracker that keeps track of the latest edits to the document. */ export class LastEditedTrackerDataObject extends DataObject implements IProvideFluidLastEditedTracker { private static readonly factory = new DataObjectFactory( - LastEditedTrackerDataObjectName, + "@fluidframework/last-edited-experimental", LastEditedTrackerDataObject, [SharedSummaryBlock.getFactory()], {}, @@ -42,12 +38,12 @@ export class LastEditedTrackerDataObject extends DataObject public get IFluidLastEditedTracker() { return this.lastEditedTracker; } - protected async componentInitializingFirstTime() { + protected async initializingFirstTime() { const sharedSummaryBlock = SharedSummaryBlock.create(this.runtime); this.root.set(this.sharedSummaryBlockId, sharedSummaryBlock.handle); } - protected async componentHasInitialized() { + protected async hasInitialized() { // hasInitialized const sharedSummaryBlock = await this.root.get>(this.sharedSummaryBlockId).get(); this._lastEditedTracker = new LastEditedTracker(sharedSummaryBlock); diff --git a/packages/framework/last-edited-experimental/src/setup.ts b/packages/framework/last-edited-experimental/src/setup.ts index bd2843abf8fa..ec8cd04ad295 100644 --- a/packages/framework/last-edited-experimental/src/setup.ts +++ b/packages/framework/last-edited-experimental/src/setup.ts @@ -6,7 +6,6 @@ import { ISequencedDocumentMessage, IQuorum } from "@fluidframework/protocol-definitions"; import { ContainerMessageType } from "@fluidframework/container-runtime"; import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; -import { requestFluidObject } from "@fluidframework/runtime-utils"; import { ILastEditDetails, IFluidLastEditedTracker } from "./interfaces"; // Default implementation of the shouldDiscardMessageFn function below that tells that all messages other @@ -50,16 +49,11 @@ function getLastEditDetailsFromMessage( * @param runtime - The container runtime whose messages are to be tracked. * @param shouldDiscardMessageFn - Function that tells if a message should not be considered in computing last edited. */ -export async function setupLastEditedTrackerForContainer( - componentId: string, +export function setupLastEditedTrackerForContainer( + lastEditedTracker: IFluidLastEditedTracker, runtime: IContainerRuntime, shouldDiscardMessageFn: (message: ISequencedDocumentMessage) => boolean = shouldDiscardMessageDefault, ) { - // eslint-disable-next-line prefer-const - let lastEditedTracker: IFluidLastEditedTracker | undefined; - // Stores the last edit details until the component has loaded. - let pendingLastEditDetails: ILastEditDetails | undefined; - // Register an op listener on the runtime. If the component has loaded, it passes the last edited information to its // last edited tracker. If the component hasn't loaded, store the last edited information temporarily. runtime.on("op", (message: ISequencedDocumentMessage) => { @@ -78,24 +72,6 @@ export async function setupLastEditedTrackerForContainer( return; } - if (lastEditedTracker !== undefined) { - // Update the last edited tracker if the component has loaded. - lastEditedTracker.updateLastEditDetails(lastEditDetails); - } else { - // If the component hasn't loaded, store the last edited details temporarily. - pendingLastEditDetails = lastEditDetails; - } + lastEditedTracker.updateLastEditDetails(lastEditDetails); }); - - // Get the last edited tracker from the component. - const component = await requestFluidObject(runtime.IFluidHandleContext, componentId); - lastEditedTracker = component.IFluidLastEditedTracker; - if (lastEditedTracker === undefined) { - throw new Error(`Component with id ${componentId} does not have IComponentLastEditedTracker.`); - } - - // Now that the component has loaded, pass any pending last edit details to its last edited tracker. - if (pendingLastEditDetails !== undefined) { - lastEditedTracker.updateLastEditDetails(pendingLastEditDetails); - } } diff --git a/packages/framework/request-handler/src/requestHandlers.ts b/packages/framework/request-handler/src/requestHandlers.ts index c00a88cac280..0b3bd56d6e93 100644 --- a/packages/framework/request-handler/src/requestHandlers.ts +++ b/packages/framework/request-handler/src/requestHandlers.ts @@ -2,8 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { IFluidObject, IResponse, IRequest } from "@fluidframework/core-interfaces"; + +import assert from "assert"; +import { + IFluidObject, + IResponse, + IRequest, + IFluidHandle, + IFluidLoadable, +} from "@fluidframework/core-interfaces"; import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; +import { IContainerRuntimeBase } from "@fluidframework/runtime-definitions"; import { RequestParser } from "@fluidframework/runtime-utils"; /** @@ -26,8 +35,41 @@ export type RuntimeRequestHandler = (request: RequestParser, runtime: IContainer * that will allow any GC policy to be implemented by container authors.) */ export const deprecated_innerRequestHandler = async (request: IRequest, runtime: IContainerRuntime) => - runtime.resolveHandle(request); + runtime.IFluidHandleContext.resolveHandle(request); export const createComponentResponse = (component: IFluidObject) => { return { status: 200, mimeType: "fluid/object", value: component }; }; + +class LegacyUriHandle implements IFluidHandle { + public readonly isAttached = true; + + public get IFluidHandle(): IFluidHandle { return this; } + + public constructor(public readonly absolutePath, public readonly runtime: IContainerRuntimeBase) { + } + + public attachGraph() { + assert(false); + } + + public async get(): Promise { + const response = await this.runtime.IFluidHandleContext.resolveHandle({ url: this.absolutePath }); + if (response.status === 200 && response.mimeType === "fluid/object") { + return response.value; + } + throw new Error(`Failed to resolve container path ${this.absolutePath}`); + } + + public bind(handle: IFluidHandle) { + throw new Error("Cannot bind to LegacyUriHandle"); + } +} + +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export function handleFromLegacyUri( + uri: string, + runtime: IContainerRuntimeBase): IFluidHandle +{ + return new LegacyUriHandle(uri, runtime); +} diff --git a/packages/framework/request-handler/src/test/requestHandlers.spec.ts b/packages/framework/request-handler/src/test/requestHandlers.spec.ts index 1263507994e4..6e6c7dc9d8af 100644 --- a/packages/framework/request-handler/src/test/requestHandlers.spec.ts +++ b/packages/framework/request-handler/src/test/requestHandlers.spec.ts @@ -5,7 +5,12 @@ /* eslint-disable @typescript-eslint/consistent-type-assertions */ import assert from "assert"; -import { IRequest, IResponse, IFluidObject, IFluidRouter } from "@fluidframework/core-interfaces"; +import { + IRequest, + IResponse, + IFluidObject, + IFluidRouter, +} from "@fluidframework/core-interfaces"; import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; import { IFluidDataStoreChannel } from "@fluidframework/runtime-definitions"; import { RequestParser } from "@fluidframework/runtime-utils"; @@ -15,6 +20,8 @@ import { } from "../requestHandlers"; class MockRuntime { + public get IFluidHandleContext() { return this; } + public async getRootDataStore(id, wait): Promise { if (id === "componentId") { return { diff --git a/packages/framework/synthesize/src/test/dependencyContainer.spec.ts b/packages/framework/synthesize/src/test/dependencyContainer.spec.ts index fa1d9fa75751..d9ae8403706e 100644 --- a/packages/framework/synthesize/src/test/dependencyContainer.spec.ts +++ b/packages/framework/synthesize/src/test/dependencyContainer.spec.ts @@ -15,16 +15,14 @@ import { FluidOjectHandle } from "@fluidframework/datastore"; import { DependencyContainer } from ".."; const mockHandleContext: IFluidHandleContext = { - path: "", absolutePath: "", isAttached: false, - IFluidRouter: undefined as any, IFluidHandleContext: undefined as any, attachGraph: () => { throw new Error("Method not implemented."); }, - request: () => { + resolveHandle: () => { throw new Error("Method not implemented."); }, }; diff --git a/packages/loader/core-interfaces/src/handles.ts b/packages/loader/core-interfaces/src/handles.ts index 3889ff428556..8348c705b96c 100644 --- a/packages/loader/core-interfaces/src/handles.ts +++ b/packages/loader/core-interfaces/src/handles.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { IFluidRouter } from "./fluidRouter"; +import { IRequest, IResponse } from "./fluidRouter"; import { IFluidObject } from "./fluidObject"; import { IFluidLoadable } from "./fluidLoadable"; @@ -16,13 +16,7 @@ export interface IProvideFluidHandleContext { /** * An IFluidHandleContext describes a routing context from which other IFluidHandleContexts are defined */ -export interface IFluidHandleContext extends IFluidRouter, IProvideFluidHandleContext { - /** - * @deprecated - Use `absolutePath` to get the path to the handle context from the root. - * Path to the handle context relative to the routeContext - */ - path: string; - +export interface IFluidHandleContext extends IProvideFluidHandleContext { /** * The absolute path to the handle context from the root. */ @@ -43,6 +37,8 @@ export interface IFluidHandleContext extends IFluidRouter, IProvideFluidHandleCo * Runs through the graph and attach the bounded handles. */ attachGraph(): void; + + resolveHandle(request: IRequest): Promise; } export const IFluidHandle: keyof IProvideFluidHandle = "IFluidHandle"; @@ -57,7 +53,23 @@ export interface IProvideFluidHandle { export interface IFluidHandle< // REVIEW: Constrain `T` to `IFluidObject & IFluidLoadable`? T = IFluidObject & IFluidLoadable - > extends IFluidHandleContext, IProvideFluidHandle { + > extends IProvideFluidHandle { + + /** + * The absolute path to the handle context from the root. + */ + absolutePath: string; + + /** + * Flag indicating whether or not the entity has services attached. + */ + isAttached: boolean; + + /** + * Runs through the graph and attach the bounded handles. + */ + attachGraph(): void; + /** * Returns a promise to the Fluid Object referenced by the handle. */ diff --git a/packages/runtime/component-runtime/src/componentRuntime.ts b/packages/runtime/component-runtime/src/componentRuntime.ts index f9f7e788e028..619159c7d652 100644 --- a/packages/runtime/component-runtime/src/componentRuntime.ts +++ b/packages/runtime/component-runtime/src/componentRuntime.ts @@ -49,7 +49,7 @@ import { ISummaryTreeWithStats, CreateSummarizerNodeSource, } from "@fluidframework/runtime-definitions"; -import { generateHandleContextPath, SummaryTreeBuilder } from "@fluidframework/runtime-utils"; +import { generateHandleContextPath, SummaryTreeBuilder, requestFluidObject } from "@fluidframework/runtime-utils"; import { IChannel, IFluidDataStoreRuntime, IChannelFactory } from "@fluidframework/datastore-definitions"; import { v4 as uuid } from "uuid"; import { IChannelContext, snapshotChannel } from "./channelContext"; @@ -241,6 +241,10 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto this.emit("dispose"); } + public async resolveHandle(request: IRequest): Promise { + return this.request(request); + } + public async request(request: IRequest): Promise { // Parse out the leading slash const id = request.url.startsWith("/") ? request.url.substr(1) : request.url; diff --git a/packages/runtime/container-runtime-definitions/src/containerRuntime.ts b/packages/runtime/container-runtime-definitions/src/containerRuntime.ts index 4653762ae167..4526d8d64b34 100644 --- a/packages/runtime/container-runtime-definitions/src/containerRuntime.ts +++ b/packages/runtime/container-runtime-definitions/src/containerRuntime.ts @@ -6,8 +6,6 @@ import { IFluidObject, IFluidRouter, - IRequest, - IResponse, } from "@fluidframework/core-interfaces"; import { IAudience, @@ -135,10 +133,4 @@ export interface IContainerRuntime extends * @param relativeUrl - A relative request within the container */ getAbsoluteUrl(relativeUrl: string): Promise; - - /** - * Resolves handle URI - * @param request - request to resolve - */ - resolveHandle(request: IRequest): Promise; } diff --git a/packages/runtime/container-runtime/src/dataStoreHandleContext.ts b/packages/runtime/container-runtime/src/containerHandleContext.ts similarity index 80% rename from packages/runtime/container-runtime/src/dataStoreHandleContext.ts rename to packages/runtime/container-runtime/src/containerHandleContext.ts index ce0750ff3be7..66a4517967b9 100644 --- a/packages/runtime/container-runtime/src/dataStoreHandleContext.ts +++ b/packages/runtime/container-runtime/src/containerHandleContext.ts @@ -9,24 +9,24 @@ import { IRequest, IResponse, } from "@fluidframework/core-interfaces"; -import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; import { AttachState } from "@fluidframework/container-definitions"; import { generateHandleContextPath } from "@fluidframework/runtime-utils"; +import { ContainerRuntime } from "./containerRuntime"; -export class FluidHandleContext implements IFluidHandleContext { +export class ContainerFluidHandleContext implements IFluidHandleContext { public get IFluidRouter() { return this; } public get IFluidHandleContext() { return this; } public readonly absolutePath: string; /** - * Creates a new FluidHandleContext. + * Creates a new ContainerFluidHandleContext. * @param path - The path to this handle relative to the routeContext. * @param runtime - The IRuntime object this context represents. * @param routeContext - The parent IFluidHandleContext that has a route to this handle. */ constructor( public readonly path: string, - private readonly runtime: IContainerRuntime, + private readonly runtime: ContainerRuntime, public readonly routeContext?: IFluidHandleContext, ) { this.absolutePath = generateHandleContextPath(path, this.routeContext); @@ -41,7 +41,7 @@ export class FluidHandleContext implements IFluidHandleContext { return this.runtime.attachState !== AttachState.Detached; } - public async request(request: IRequest): Promise { + public async resolveHandle(request: IRequest): Promise { return this.runtime.resolveHandle(request); } } diff --git a/packages/runtime/container-runtime/src/containerRuntime.ts b/packages/runtime/container-runtime/src/containerRuntime.ts index 963c0da3fdda..86784640683c 100644 --- a/packages/runtime/container-runtime/src/containerRuntime.ts +++ b/packages/runtime/container-runtime/src/containerRuntime.ts @@ -86,6 +86,8 @@ import { CreateChildSummarizerNodeFn, CreateChildSummarizerNodeParam, CreateSummarizerNodeSource, + IAgentScheduler, + ITaskManager, } from "@fluidframework/runtime-definitions"; import { FluidSerializer, @@ -94,6 +96,7 @@ import { SummarizerNode, convertToSummaryTree, RequestParser, + requestFluidObject, } from "@fluidframework/runtime-utils"; import { v4 as uuid } from "uuid"; import { @@ -102,7 +105,7 @@ import { RemotedFluidDataStoreContext, createAttributesBlob, } from "./dataStoreContext"; -import { FluidHandleContext } from "./dataStoreHandleContext"; +import { ContainerFluidHandleContext } from "./containerHandleContext"; import { FluidDataStoreRegistry } from "./dataStoreRegistry"; import { debug } from "./debug"; import { ISummarizerRuntime, Summarizer } from "./summarizer"; @@ -673,7 +676,7 @@ export class ContainerRuntime extends EventEmitter this._connected = this.context.connected; this.chunkMap = new Map(chunks); - this.IFluidHandleContext = new FluidHandleContext("", this); + this.IFluidHandleContext = new ContainerFluidHandleContext("", this); this.logger = ChildLogger.create(context.logger, undefined, { runtimeVersion: pkgVersion, @@ -1347,12 +1350,12 @@ export class ContainerRuntime extends EventEmitter private isContainerMessageDirtyable(type: ContainerMessageType, contents: any) { if (type === ContainerMessageType.Attach) { const attachMessage = contents as InboundAttachMessage; - if (attachMessage.id === SchedulerType) { + if (attachMessage.id === schedulerId) { return false; } } else if (type === ContainerMessageType.FluidDataStoreOp) { const envelope = contents as IEnvelope; - if (envelope.address === SchedulerType) { + if (envelope.address === schedulerId) { return false; } } @@ -1903,12 +1906,15 @@ export class ContainerRuntime extends EventEmitter } } - private async getScheduler() { - const schedulerRuntime = await this.getDataStore(schedulerId, true); - const schedulerResponse = await schedulerRuntime.request({ url: "" }); - const schedulerFluidDataStore = schedulerResponse.value as IFluidObject; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return schedulerFluidDataStore.IAgentScheduler!; + public async getTaskManager(): Promise { + return requestFluidObject( + await this.getDataStore(schedulerId, true), + ""); + } + + public async getScheduler(): Promise { + const taskManager = await this.getTaskManager(); + return taskManager.IAgentScheduler; } private updateLeader(leadership: boolean) { diff --git a/packages/runtime/container-runtime/src/summarizerHandle.ts b/packages/runtime/container-runtime/src/summarizerHandle.ts index 270d2a46c34f..851316cd9c06 100644 --- a/packages/runtime/container-runtime/src/summarizerHandle.ts +++ b/packages/runtime/container-runtime/src/summarizerHandle.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ import { FluidOjectHandle } from "@fluidframework/datastore"; -import { IFluidHandle, IRequest, IResponse } from "@fluidframework/core-interfaces"; +import { IFluidHandle } from "@fluidframework/core-interfaces"; // TODO #2425 Expose Summarizer handle as FluidOjectHandle w/ tests export class SummarizerHandle extends FluidOjectHandle { @@ -18,8 +18,4 @@ export class SummarizerHandle extends FluidOjectHandle { public bind(handle: IFluidHandle) { return; } - - public async request(request: IRequest): Promise { - throw Error("Do not try to request on a summarizer handle object."); - } } diff --git a/packages/runtime/container-runtime/src/test/summarizerHandle.spec.ts b/packages/runtime/container-runtime/src/test/summarizerHandle.spec.ts index 585f33150501..8e9399b32b14 100644 --- a/packages/runtime/container-runtime/src/test/summarizerHandle.spec.ts +++ b/packages/runtime/container-runtime/src/test/summarizerHandle.spec.ts @@ -11,16 +11,14 @@ import { import { SummarizerHandle } from "../summarizerHandle"; const mockHandleContext: IFluidHandleContext = { - path: "", absolutePath: "", isAttached: false, - IFluidRouter: undefined as any, IFluidHandleContext: undefined as any, attachGraph: () => { throw new Error("Method not implemented."); }, - request: () => { + resolveHandle: () => { throw new Error("Method not implemented."); }, }; @@ -43,11 +41,4 @@ describe("SummarizerHandle", () => { assert(e.message === "Do not try to get a summarizer object from the handle. Reference it directly."); } }); - it("request should fail", async () => { - try { - await handle?.request({} as any); - } catch (e) { - assert(e.message === "Do not try to request on a summarizer handle object."); - } - }); }); diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index 511e93f39b90..b7878f971fbc 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -241,6 +241,10 @@ export class FluidDataStoreRuntime extends EventEmitter implements IFluidDataSto this.emit("dispose"); } + public async resolveHandle(request: IRequest): Promise { + return this.request(request); + } + public async request(request: IRequest): Promise { // Parse out the leading slash const id = request.url.startsWith("/") ? request.url.substr(1) : request.url; diff --git a/packages/runtime/datastore/src/fluidHandle.ts b/packages/runtime/datastore/src/fluidHandle.ts index 55ed69444a09..8626d9ca7dc0 100644 --- a/packages/runtime/datastore/src/fluidHandle.ts +++ b/packages/runtime/datastore/src/fluidHandle.ts @@ -7,9 +7,6 @@ import { IFluidObject, IFluidHandle, IFluidHandleContext, - IFluidRouter, - IRequest, - IResponse, } from "@fluidframework/core-interfaces"; import { AttachState } from "@fluidframework/container-definitions"; import { generateHandleContextPath } from "@fluidframework/runtime-utils"; @@ -20,8 +17,6 @@ export class FluidOjectHandle implements private bound: Set | undefined; public readonly absolutePath: string; - public get IFluidRouter(): IFluidRouter { return this; } - public get IFluidHandleContext(): IFluidHandleContext { return this; } public get IFluidHandle(): IFluidHandle { return this; } public get isAttached(): boolean { @@ -76,12 +71,4 @@ export class FluidOjectHandle implements this.bound.add(handle); } - - public async request(request: IRequest): Promise { - if (this.value.IFluidRouter !== undefined) { - return this.value.IFluidRouter.request(request); - } else { - return { status: 404, mimeType: "text/plain", value: `${request.url} not found` }; - } - } } diff --git a/packages/runtime/runtime-definitions/src/agent.ts b/packages/runtime/runtime-definitions/src/agent.ts index 945b3ad3a65f..3565a8d3cf06 100644 --- a/packages/runtime/runtime-definitions/src/agent.ts +++ b/packages/runtime/runtime-definitions/src/agent.ts @@ -34,6 +34,11 @@ export interface IProvideTaskManager { * Task manager enables app to register and pick tasks. */ export interface ITaskManager extends IProvideTaskManager, IFluidLoadable, IFluidRouter { + /** + * access to IAgentScheduler + */ + readonly IAgentScheduler: IAgentScheduler; + /** * Registers tasks task so that the client can run the task later. */ diff --git a/packages/runtime/runtime-definitions/src/componentContext.ts b/packages/runtime/runtime-definitions/src/componentContext.ts index 6becafa51ec0..e03c259c6a67 100644 --- a/packages/runtime/runtime-definitions/src/componentContext.ts +++ b/packages/runtime/runtime-definitions/src/componentContext.ts @@ -32,6 +32,7 @@ import { import { IProvideFluidDataStoreRegistry } from "./componentRegistry"; import { IInboundSignalMessage } from "./protocol"; import { ISummaryTreeWithStats, ISummarizerNode, SummarizeInternalFn, CreateChildSummarizerNodeParam } from "./summary"; +import { ITaskManager } from "./agent"; /** * Runtime flush mode handling @@ -114,6 +115,8 @@ export interface IContainerRuntimeBase extends * @param relativeUrl - A relative request within the container */ getAbsoluteUrl(relativeUrl: string): Promise; + + getTaskManager(): Promise; } /** diff --git a/packages/runtime/runtime-utils/src/remoteComponentHandle.ts b/packages/runtime/runtime-utils/src/remoteComponentHandle.ts index e7771d0fc879..62ed128cfba7 100644 --- a/packages/runtime/runtime-utils/src/remoteComponentHandle.ts +++ b/packages/runtime/runtime-utils/src/remoteComponentHandle.ts @@ -45,7 +45,7 @@ export class RemoteFluidObjectHandle implements IFluidHandle { public async get(): Promise { if (this.componentP === undefined) { - this.componentP = this.routeContext.request({ url: this.absolutePath }) + this.componentP = this.routeContext.resolveHandle({ url: this.absolutePath }) .then((response) => response.mimeType === "fluid/object" ? response.value as IFluidObject diff --git a/packages/runtime/runtime-utils/src/serializer.ts b/packages/runtime/runtime-utils/src/serializer.ts index 135b6ea51c1d..576d7d8127cf 100644 --- a/packages/runtime/runtime-utils/src/serializer.ts +++ b/packages/runtime/runtime-utils/src/serializer.ts @@ -11,25 +11,6 @@ import { import { RemoteFluidObjectHandle } from "./remoteComponentHandle"; import { isSerializedHandle } from "./utils"; -/** - * 0.21 back-compat - * Retrieves the absolute URL for a handle - */ -function toAbsoluteUrl(handle: IFluidHandle): string { - let result = ""; - let context: IFluidHandleContext | undefined = handle; - - while (context !== undefined) { - if (context.path !== "") { - result = `/${context.path}${result}`; - } - - context = context.routeContext; - } - - return result; -} - /** * Component serializer implementation */ @@ -138,20 +119,9 @@ export class FluidSerializer implements IFluidSerializer { private serializeHandle(handle: IFluidHandle, context: IFluidHandleContext, bind: IFluidHandle) { bind.bind(handle); - let url: string; - - if ("absolutePath" in handle) { - url = handle.absolutePath; - } else { - // 0.21 back-compat - // 0.21 and earlier version do not have `absolutePath` so we genrate the absolute path from the - // routeContext's `path`. - url = toAbsoluteUrl(handle); - } - return { type: "__fluid_handle__", - url, + url: handle.absolutePath, }; } } diff --git a/packages/runtime/runtime-utils/src/test/utils.ts b/packages/runtime/runtime-utils/src/test/utils.ts index 28795c9d309a..4a68835f891d 100644 --- a/packages/runtime/runtime-utils/src/test/utils.ts +++ b/packages/runtime/runtime-utils/src/test/utils.ts @@ -7,16 +7,14 @@ import { IFluidHandle, IFluidHandleContext } from "@fluidframework/core-interfac import { RemoteFluidObjectHandle } from "../remoteComponentHandle"; export const mockHandleContext: IFluidHandleContext = { - path: "", absolutePath: "", isAttached: false, - IFluidRouter: undefined as any, IFluidHandleContext: undefined as any, attachGraph: () => { throw new Error("Method not implemented."); }, - request: () => { + resolveHandle: () => { throw new Error("Method not implemented."); }, }; diff --git a/packages/runtime/test-runtime-utils/src/mocks.ts b/packages/runtime/test-runtime-utils/src/mocks.ts index 549066fd73e8..5dd9ded141d1 100644 --- a/packages/runtime/test-runtime-utils/src/mocks.ts +++ b/packages/runtime/test-runtime-utils/src/mocks.ts @@ -494,6 +494,10 @@ export class MockFluidDataStoreRuntime extends EventEmitter return; } + public async resolveHandle(request: IRequest): Promise { + return this.request(request); + } + public async request(request: IRequest): Promise { return null; } diff --git a/packages/test/end-to-end-tests/src/test/agentScheduler.spec.ts b/packages/test/end-to-end-tests/src/test/agentScheduler.spec.ts index 61da55b82fa6..3f31ab6396e1 100644 --- a/packages/test/end-to-end-tests/src/test/agentScheduler.spec.ts +++ b/packages/test/end-to-end-tests/src/test/agentScheduler.spec.ts @@ -7,7 +7,8 @@ import assert from "assert"; import { AgentSchedulerFactory, TaskManager } from "@fluidframework/agent-scheduler"; import { IFluidCodeDetails, ILoader } from "@fluidframework/container-definitions"; import { Container } from "@fluidframework/container-loader"; -import { IAgentScheduler, SchedulerType } from "@fluidframework/runtime-definitions"; +import { IAgentScheduler } from "@fluidframework/runtime-definitions"; +import { schedulerId } from "@fluidframework/container-runtime"; import { LocalDeltaConnectionServer, ILocalDeltaConnectionServer } from "@fluidframework/server-local-server"; import { createLocalLoader, OpProcessingController, initializeLocalContainer } from "@fluidframework/test-utils"; @@ -45,7 +46,7 @@ describe("AgentScheduler", () => { deltaConnectionServer = LocalDeltaConnectionServer.create(); const container = await createContainer(); - scheduler = await requestFluidObject(`${SchedulerType}`, container) + scheduler = await requestFluidObject(schedulerId, container) .then((taskManager) => taskManager.IAgentScheduler); // Make sure all initial ops (around leadership) are processed. @@ -132,11 +133,11 @@ describe("AgentScheduler", () => { deltaConnectionServer = LocalDeltaConnectionServer.create(); container1 = await createContainer(); - scheduler1 = await requestFluidObject(`${SchedulerType}`, container1) + scheduler1 = await requestFluidObject(schedulerId, container1) .then((taskManager) => taskManager.IAgentScheduler); container2 = await createContainer(); - scheduler2 = await requestFluidObject(`${SchedulerType}`, container2) + scheduler2 = await requestFluidObject(schedulerId, container2) .then((taskManager) => taskManager.IAgentScheduler); // Make sure all initial ops (around leadership) are processed. diff --git a/packages/test/end-to-end-tests/src/test/batching.spec.ts b/packages/test/end-to-end-tests/src/test/batching.spec.ts index ca5b2df2a3ab..3bca7910a5d6 100644 --- a/packages/test/end-to-end-tests/src/test/batching.spec.ts +++ b/packages/test/end-to-end-tests/src/test/batching.spec.ts @@ -6,11 +6,11 @@ import assert from "assert"; import { IFluidCodeDetails } from "@fluidframework/container-definitions"; import { Container } from "@fluidframework/container-loader"; -import { ContainerMessageType } from "@fluidframework/container-runtime"; +import { ContainerMessageType, schedulerId } from "@fluidframework/container-runtime"; import { IContainerRuntime } from "@fluidframework/container-runtime-definitions"; import { SharedMap } from "@fluidframework/map"; import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions"; -import { IEnvelope, SchedulerType, FlushMode } from "@fluidframework/runtime-definitions"; +import { IEnvelope, FlushMode } from "@fluidframework/runtime-definitions"; import { ILocalDeltaConnectionServer, LocalDeltaConnectionServer } from "@fluidframework/server-local-server"; import { createLocalLoader, @@ -61,7 +61,7 @@ describe("Batching", () => { component.context.containerRuntime.on("op", (message: ISequencedDocumentMessage) => { if (message.type === ContainerMessageType.FluidDataStoreOp) { const envelope = message.contents as IEnvelope; - if (envelope.address !== `${SchedulerType}`) { + if (envelope.address !== schedulerId) { receivedMessages.push(message); } } diff --git a/packages/test/end-to-end-tests/src/test/opsOnReconnect.spec.ts b/packages/test/end-to-end-tests/src/test/opsOnReconnect.spec.ts index 7f2cd4fde25e..5d43f4b9bada 100644 --- a/packages/test/end-to-end-tests/src/test/opsOnReconnect.spec.ts +++ b/packages/test/end-to-end-tests/src/test/opsOnReconnect.spec.ts @@ -12,7 +12,7 @@ import { IContainerRuntime } from "@fluidframework/container-runtime-definitions import { LocalDocumentServiceFactory, LocalResolver } from "@fluidframework/local-driver"; import { SharedMap, SharedDirectory } from "@fluidframework/map"; import { ISequencedDocumentMessage, ConnectionState } from "@fluidframework/protocol-definitions"; -import { IEnvelope, SchedulerType, FlushMode } from "@fluidframework/runtime-definitions"; +import { IEnvelope, FlushMode } from "@fluidframework/runtime-definitions"; import { ILocalDeltaConnectionServer, LocalDeltaConnectionServer } from "@fluidframework/server-local-server"; import { SharedString } from "@fluidframework/sequence"; import { @@ -26,6 +26,7 @@ import { ContainerMessageType, isRuntimeMessage, unpackRuntimeMessage, + schedulerId, } from "@fluidframework/container-runtime"; import { requestFluidObject } from "@fluidframework/runtime-utils"; @@ -101,7 +102,7 @@ describe("Ops on Reconnect", () => { const message = unpackRuntimeMessage(containerMessage); if (message.type === ContainerMessageType.FluidDataStoreOp) { const envelope = message.contents as IEnvelope; - if (envelope.address !== `${SchedulerType}`) { + if (envelope.address !== schedulerId) { // The client ID of firstContainer should have changed on disconnect. assert.notEqual( message.clientId, firstContainerClientId, "The clientId did not change after disconnect");